Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d511dbdd66 | ||
|
|
9e9ea0cfd5 | ||
|
|
bcebe2ebed | ||
|
|
a43fd9cedf | ||
|
|
a5e4e55df5 | ||
|
|
c4ca101594 | ||
|
|
985c9ce07e | ||
|
|
790e8e1dbc | ||
|
|
ef6284d5cc | ||
|
|
328825293a | ||
|
|
4e981c140b | ||
|
|
8940cb275b |
No files matched your search
@@ -16,6 +16,32 @@ Routing, Mails) stehen in `.ai/conventions.md` und werden hier eingebunden — *
|
|||||||
- [Zahlungsmodule](app/EventPaymentModules/CLAUDE.md) — interface-gesteuerte Zahlungsarten (Optionen, Config,
|
- [Zahlungsmodule](app/EventPaymentModules/CLAUDE.md) — interface-gesteuerte Zahlungsarten (Optionen, Config,
|
||||||
Anmelde-Zusammenfassung, GiroCode/Fähigkeits-Interfaces, neues Modul hinzufügen).
|
Anmelde-Zusammenfassung, GiroCode/Fähigkeits-Interfaces, neues Modul hinzufügen).
|
||||||
|
|
||||||
|
## Local Group / Parent Group (Stamm / Bund)
|
||||||
|
|
||||||
|
Technisch heißt es **Local Group** (Mandant/Tenant) und **Parent Group** (übergeordnete Gruppe); „Stamm"/„Bund" sind nur
|
||||||
|
UI-Bezeichnungen.
|
||||||
|
|
||||||
|
- **ParentGroup** (`App\Models\ParentGroup`, Tabelle `parent_groups`, app-weit): `tenants.parent_group_id` ist Pflicht
|
||||||
|
(n:1). `is_fallback` markiert „Ohne Zuordnung" — `Tenant::booted()` setzt sie, wenn nichts angegeben ist; sie ist nie
|
||||||
|
löschbar. Andere nur, solange weder Tenant noch Anmeldung sie nutzt (`AdminParentGroupRepository::isInUse()`).
|
||||||
|
Verwaltung LV-only unter `/admin/parent-groups`; Zuordnung am Tenant im Tab „Allgemeines" und bei der Anlage.
|
||||||
|
- **Event**: `participant_group_type` (Enum `App\Enumerations\ParticipantGroupType`: `local_group` | `parent_group`),
|
||||||
|
gewählt bei der Anlage, in den Einstellungen änderbar nur ohne Anmeldungen. **Einzige Quelle für die Auswahl**:
|
||||||
|
`Event::participantGroupOptions()` — teilnehmende Stämme oder deren Bünde (distinct). Gespeichert wird
|
||||||
|
`event_participants.local_group` (Slug) **oder** `event_participants.parent_group_id`.
|
||||||
|
- **Anzeige**: `EventParticipant::participantGroupName()` → Resource-Feld `participantGroup`; `EventResource` liefert
|
||||||
|
`participantGroupType`, `participantGroupLabel`, `participantGroupOptions` (`{id, name, value}`).
|
||||||
|
- **Bezeichnungen** in `global_settings` (Model `GlobalSetting`, Repository `GlobalSettingRepository`, im Container
|
||||||
|
`scoped` — immer über `app(...)` beziehen), Singular + Plural je Ebene, gepflegt von Hauptadmins unter
|
||||||
|
`/admin/settings`. Gelesen über `ParticipantGroupType::label()/pluralLabel()/labels()`. Texte ohne Artikel formulieren
|
||||||
|
(„Bitte bei „{label}" eine Auswahl treffen."), weil der Artikel vom Wort abhängt.
|
||||||
|
|
||||||
|
## eFZ-Prüfung pro Veranstaltung
|
||||||
|
|
||||||
|
`events.coc_check_required` (Default `true`, Checkbox in `CommonSettings.vue`, Resource-Feld `cocCheckRequired`). Ist
|
||||||
|
sie aus, liefert `CertificateOfConductionCheckCommand` ohne API-Aufruf `not_required`, und `SignUpCommand` belegt
|
||||||
|
`efz_status` mit `not_required` vor. Wirkt nur auf neue Anmeldungen, bestehende Status bleiben.
|
||||||
|
|
||||||
## Umsatzsteuer / Steuerinformationen
|
## Umsatzsteuer / Steuerinformationen
|
||||||
|
|
||||||
Rechtlich korrekter Begriff ist **Umsatzsteuer (USt)**, nicht MwSt. Ziel: tagesbasierte Anmelde-Rechnungen („Teilnahme
|
Rechtlich korrekter Begriff ist **Umsatzsteuer (USt)**, nicht MwSt. Ziel: tagesbasierte Anmelde-Rechnungen („Teilnahme
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Admin\Actions\CreateParentGroup;
|
||||||
|
|
||||||
|
use App\Enumerations\ParticipantGroupType;
|
||||||
|
use App\Models\ParentGroup;
|
||||||
|
|
||||||
|
class CreateParentGroupAction
|
||||||
|
{
|
||||||
|
public function __construct(private CreateParentGroupRequest $request)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function execute(): CreateParentGroupResponse
|
||||||
|
{
|
||||||
|
$response = new CreateParentGroupResponse();
|
||||||
|
|
||||||
|
$name = trim($this->request->name);
|
||||||
|
|
||||||
|
if ($name === '') {
|
||||||
|
$response->message = 'Bitte einen Namen angeben.';
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
$response->parentGroup = ParentGroup::create([
|
||||||
|
'name' => $name,
|
||||||
|
'is_fallback' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->success = true;
|
||||||
|
$response->message = sprintf('%s „%s" wurde angelegt.', ParticipantGroupType::ParentGroup->label(), $name);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Admin\Actions\CreateParentGroup;
|
||||||
|
|
||||||
|
class CreateParentGroupRequest
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public string $name,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Admin\Actions\CreateParentGroup;
|
||||||
|
|
||||||
|
use App\Models\ParentGroup;
|
||||||
|
|
||||||
|
class CreateParentGroupResponse
|
||||||
|
{
|
||||||
|
public bool $success = false;
|
||||||
|
public string $message = '';
|
||||||
|
public ?ParentGroup $parentGroup = null;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Domains\Admin\Actions\CreateTenant;
|
namespace App\Domains\Admin\Actions\CreateTenant;
|
||||||
|
|
||||||
|
use App\Enumerations\ParticipantGroupType;
|
||||||
use App\Models\AvailablePaymentMethod;
|
use App\Models\AvailablePaymentMethod;
|
||||||
use App\Models\PaymentMethod;
|
use App\Models\PaymentMethod;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
@@ -16,10 +17,17 @@ class CreateTenantAction
|
|||||||
{
|
{
|
||||||
$response = new CreateTenantResponse();
|
$response = new CreateTenantResponse();
|
||||||
|
|
||||||
|
if ($this->request->parentGroup === null) {
|
||||||
|
$response->message = sprintf('Bitte bei „%s" eine Auswahl treffen.', ParticipantGroupType::ParentGroup->label());
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
$tenant = Tenant::create([
|
$tenant = Tenant::create([
|
||||||
'name' => $this->request->name,
|
'name' => $this->request->name,
|
||||||
'slug' => $this->request->slug,
|
'slug' => $this->request->slug,
|
||||||
'url' => $this->request->url,
|
'url' => $this->request->url,
|
||||||
|
'parent_group_id' => $this->request->parentGroup->id,
|
||||||
'email' => '',
|
'email' => '',
|
||||||
'email_finance' => '',
|
'email_finance' => '',
|
||||||
'account_name' => '',
|
'account_name' => '',
|
||||||
@@ -34,19 +42,23 @@ class CreateTenantAction
|
|||||||
$paymentMethodDefaults = PaymentMethod::defaults();
|
$paymentMethodDefaults = PaymentMethod::defaults();
|
||||||
foreach (PaymentMethod::all() as $paymentMethod) {
|
foreach (PaymentMethod::all() as $paymentMethod) {
|
||||||
$defaults = $paymentMethodDefaults[$paymentMethod->slug] ?? ['name' => $paymentMethod->slug, 'description' => null, 'configuration' => []];
|
$defaults = $paymentMethodDefaults[$paymentMethod->slug] ?? ['name' => $paymentMethod->slug, 'description' => null, 'configuration' => []];
|
||||||
|
$configuration = PaymentMethod::sanitizeConfiguration($paymentMethod->slug, $defaults['configuration'] ?? []);
|
||||||
|
|
||||||
|
// Aktiv nur, was schon vollständig konfiguriert ist -- derselbe Maßstab wie beim Aktivieren
|
||||||
|
// von Hand. Ein neuer Stamm hat noch keine Bankdaten; seine Zahlungsarten schaltet er frei,
|
||||||
|
// sobald sie gepflegt sind.
|
||||||
AvailablePaymentMethod::create([
|
AvailablePaymentMethod::create([
|
||||||
'tenant' => $tenant->slug,
|
'tenant' => $tenant->slug,
|
||||||
'slug' => $paymentMethod->slug,
|
'slug' => $paymentMethod->slug,
|
||||||
'name' => $defaults['name'],
|
'name' => $defaults['name'],
|
||||||
'description' => $defaults['description'],
|
'description' => $defaults['description'],
|
||||||
'active' => true,
|
'active' => PaymentMethod::isConfigurationComplete($paymentMethod->slug, $configuration),
|
||||||
'configuration' => PaymentMethod::sanitizeConfiguration($paymentMethod->slug, $defaults['configuration'] ?? []),
|
'configuration' => $configuration,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$response->success = true;
|
$response->success = true;
|
||||||
$response->message = 'Stamm wurde angelegt.';
|
$response->message = sprintf('%s wurde angelegt.', ParticipantGroupType::LocalGroup->label());
|
||||||
$response->slug = $tenant->slug;
|
$response->slug = $tenant->slug;
|
||||||
|
|
||||||
return $response;
|
return $response;
|
||||||
|
|||||||
@@ -2,12 +2,16 @@
|
|||||||
|
|
||||||
namespace App\Domains\Admin\Actions\CreateTenant;
|
namespace App\Domains\Admin\Actions\CreateTenant;
|
||||||
|
|
||||||
|
use App\Models\ParentGroup;
|
||||||
|
|
||||||
class CreateTenantRequest
|
class CreateTenantRequest
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public string $name,
|
public string $name,
|
||||||
public string $slug,
|
public string $slug,
|
||||||
public string $url,
|
public string $url,
|
||||||
|
/** Pflicht; null heißt, die übermittelte ID war unbekannt. */
|
||||||
|
public ?ParentGroup $parentGroup,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Admin\Actions\DeleteParentGroup;
|
||||||
|
|
||||||
|
class DeleteParentGroupAction
|
||||||
|
{
|
||||||
|
public function __construct(private DeleteParentGroupRequest $request)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function execute(): DeleteParentGroupResponse
|
||||||
|
{
|
||||||
|
$response = new DeleteParentGroupResponse();
|
||||||
|
|
||||||
|
// Die Fallback-Gruppe nimmt Mandanten ohne Zuordnung auf und muss deshalb bestehen bleiben.
|
||||||
|
if ($this->request->parentGroup->is_fallback) {
|
||||||
|
$response->message = sprintf('„%s" kann nicht gelöscht werden.', $this->request->parentGroup->name);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->request->inUse) {
|
||||||
|
$response->message = sprintf(
|
||||||
|
'„%s" ist noch zugeordnet und kann deshalb nicht gelöscht werden.',
|
||||||
|
$this->request->parentGroup->name,
|
||||||
|
);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->request->parentGroup->delete();
|
||||||
|
|
||||||
|
$response->success = true;
|
||||||
|
$response->message = sprintf('„%s" wurde gelöscht.', $this->request->parentGroup->name);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Admin\Actions\DeleteParentGroup;
|
||||||
|
|
||||||
|
use App\Models\ParentGroup;
|
||||||
|
|
||||||
|
class DeleteParentGroupRequest
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public ParentGroup $parentGroup,
|
||||||
|
/** Ob die Gruppe noch von einem Mandanten oder einer Anmeldung verwendet wird. */
|
||||||
|
public bool $inUse,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Admin\Actions\DeleteParentGroup;
|
||||||
|
|
||||||
|
class DeleteParentGroupResponse
|
||||||
|
{
|
||||||
|
public bool $success = false;
|
||||||
|
public string $message = '';
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Admin\Actions\RenameParentGroup;
|
||||||
|
|
||||||
|
class RenameParentGroupAction
|
||||||
|
{
|
||||||
|
public function __construct(private RenameParentGroupRequest $request)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function execute(): RenameParentGroupResponse
|
||||||
|
{
|
||||||
|
$response = new RenameParentGroupResponse();
|
||||||
|
|
||||||
|
$name = trim($this->request->name);
|
||||||
|
|
||||||
|
if ($name === '') {
|
||||||
|
$response->message = 'Bitte einen Namen angeben.';
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Umbenennen ist jederzeit möglich: Mandanten und Anmeldungen verweisen über die ID.
|
||||||
|
$this->request->parentGroup->update(['name' => $name]);
|
||||||
|
|
||||||
|
$response->success = true;
|
||||||
|
$response->message = 'Der Name wurde gespeichert.';
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Admin\Actions\RenameParentGroup;
|
||||||
|
|
||||||
|
use App\Models\ParentGroup;
|
||||||
|
|
||||||
|
class RenameParentGroupRequest
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public ParentGroup $parentGroup,
|
||||||
|
public string $name,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Admin\Actions\RenameParentGroup;
|
||||||
|
|
||||||
|
class RenameParentGroupResponse
|
||||||
|
{
|
||||||
|
public bool $success = false;
|
||||||
|
public string $message = '';
|
||||||
|
}
|
||||||
+7
-2
@@ -20,10 +20,15 @@ class UpdateAvailablePaymentMethodAction
|
|||||||
// Nur bekannte Options-Keys übernehmen -- das Options-Schema des Zahlungsmoduls ist die Autorität.
|
// Nur bekannte Options-Keys übernehmen -- das Options-Schema des Zahlungsmoduls ist die Autorität.
|
||||||
$configuration = PaymentMethod::sanitizeConfiguration($paymentMethod->slug, $this->request->configuration);
|
$configuration = PaymentMethod::sanitizeConfiguration($paymentMethod->slug, $this->request->configuration);
|
||||||
|
|
||||||
// Guard: Aktivierung nur erlaubt, wenn alle Pflicht-Optionen befüllt sind.
|
// Guard: Aktivierung nur erlaubt, wenn alle Pflicht-Optionen befüllt und gültig sind. Inaktiv
|
||||||
|
// gespeichert werden darf auch eine unfertige Konfiguration -- als Entwurf.
|
||||||
if ($this->request->active && !PaymentMethod::isConfigurationComplete($paymentMethod->slug, $configuration)) {
|
if ($this->request->active && !PaymentMethod::isConfigurationComplete($paymentMethod->slug, $configuration)) {
|
||||||
|
$errors = PaymentMethod::configurationErrors($paymentMethod->slug, $configuration);
|
||||||
|
|
||||||
$response->success = false;
|
$response->success = false;
|
||||||
$response->message = 'Bitte zuerst alle Pflichtfelder ausfüllen, bevor die Zahlungsmethode aktiviert wird.';
|
$response->message = $errors !== []
|
||||||
|
? implode(' ', $errors) . ' Die Zahlungsmethode kann so nicht aktiviert werden.'
|
||||||
|
: 'Bitte zuerst alle Pflichtfelder ausfüllen, bevor die Zahlungsmethode aktiviert wird.';
|
||||||
|
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Admin\Actions\UpdateGlobalSettings;
|
||||||
|
|
||||||
|
use App\Models\GlobalSetting;
|
||||||
|
|
||||||
|
class UpdateGlobalSettingsAction
|
||||||
|
{
|
||||||
|
public function __construct(private UpdateGlobalSettingsRequest $request)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function execute(): UpdateGlobalSettingsResponse
|
||||||
|
{
|
||||||
|
$response = new UpdateGlobalSettingsResponse();
|
||||||
|
|
||||||
|
$values = [
|
||||||
|
GlobalSetting::LOCAL_GROUP_LABEL => trim($this->request->localGroupLabel),
|
||||||
|
GlobalSetting::LOCAL_GROUP_LABEL_PLURAL => trim($this->request->localGroupLabelPlural),
|
||||||
|
GlobalSetting::PARENT_GROUP_LABEL => trim($this->request->parentGroupLabel),
|
||||||
|
GlobalSetting::PARENT_GROUP_LABEL_PLURAL => trim($this->request->parentGroupLabelPlural),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Eine leere Bezeichnung ließe Überschriften und Auswahlfelder ohne Beschriftung zurück.
|
||||||
|
if (in_array('', $values, true)) {
|
||||||
|
$response->message = 'Bitte alle Bezeichnungen ausfüllen.';
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->request->globalSettings->setMany($values);
|
||||||
|
|
||||||
|
$response->success = true;
|
||||||
|
$response->message = 'Einstellungen wurden gespeichert.';
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Admin\Actions\UpdateGlobalSettings;
|
||||||
|
|
||||||
|
use App\Repositories\GlobalSettingRepository;
|
||||||
|
|
||||||
|
class UpdateGlobalSettingsRequest
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public GlobalSettingRepository $globalSettings,
|
||||||
|
public string $localGroupLabel,
|
||||||
|
public string $localGroupLabelPlural,
|
||||||
|
public string $parentGroupLabel,
|
||||||
|
public string $parentGroupLabelPlural,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Admin\Actions\UpdateGlobalSettings;
|
||||||
|
|
||||||
|
class UpdateGlobalSettingsResponse
|
||||||
|
{
|
||||||
|
public bool $success = false;
|
||||||
|
public string $message = '';
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Domains\Admin\Actions\UpdateTenantGeneral;
|
namespace App\Domains\Admin\Actions\UpdateTenantGeneral;
|
||||||
|
|
||||||
|
use App\Enumerations\ParticipantGroupType;
|
||||||
|
|
||||||
class UpdateTenantGeneralAction
|
class UpdateTenantGeneralAction
|
||||||
{
|
{
|
||||||
public function __construct(private UpdateTenantGeneralRequest $request)
|
public function __construct(private UpdateTenantGeneralRequest $request)
|
||||||
@@ -12,10 +14,17 @@ class UpdateTenantGeneralAction
|
|||||||
{
|
{
|
||||||
$response = new UpdateTenantGeneralResponse();
|
$response = new UpdateTenantGeneralResponse();
|
||||||
|
|
||||||
|
if ($this->request->parentGroup === null) {
|
||||||
|
$response->message = sprintf('Bitte bei „%s" eine Auswahl treffen.', ParticipantGroupType::ParentGroup->label());
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
$this->request->tenant->update([
|
$this->request->tenant->update([
|
||||||
'name' => $this->request->name,
|
'name' => $this->request->name,
|
||||||
'slug' => $this->request->slug,
|
'slug' => $this->request->slug,
|
||||||
'url' => $this->request->url,
|
'url' => $this->request->url,
|
||||||
|
'parent_group_id' => $this->request->parentGroup->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response->success = true;
|
$response->success = true;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Domains\Admin\Actions\UpdateTenantGeneral;
|
namespace App\Domains\Admin\Actions\UpdateTenantGeneral;
|
||||||
|
|
||||||
|
use App\Models\ParentGroup;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
|
|
||||||
class UpdateTenantGeneralRequest
|
class UpdateTenantGeneralRequest
|
||||||
@@ -11,6 +12,8 @@ class UpdateTenantGeneralRequest
|
|||||||
public string $name,
|
public string $name,
|
||||||
public string $slug,
|
public string $slug,
|
||||||
public string $url,
|
public string $url,
|
||||||
|
/** Pflicht; null heißt, die übermittelte ID war unbekannt. */
|
||||||
|
public ?ParentGroup $parentGroup,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,12 @@ use App\Scopes\SiteScope;
|
|||||||
|
|
||||||
class UpdateTenantPaymentAction
|
class UpdateTenantPaymentAction
|
||||||
{
|
{
|
||||||
|
/** Zahlungsarten, deren Konto das Konto des Stammes ist (Überweisung: Ziel, Lastschrift: Gläubiger). */
|
||||||
|
private const array BANK_ACCOUNT_SLUGS = [
|
||||||
|
PaymentMethod::PAYMENT_ACCOUNT_TRANSACTION,
|
||||||
|
PaymentMethod::PAYMENT_SEPA_DIRECT_DEBIT,
|
||||||
|
];
|
||||||
|
|
||||||
public function __construct(private UpdateTenantPaymentRequest $request)
|
public function __construct(private UpdateTenantPaymentRequest $request)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -22,7 +28,9 @@ class UpdateTenantPaymentAction
|
|||||||
'account_name' => $this->request->accountName,
|
'account_name' => $this->request->accountName,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->syncTransferPaymentConfiguration();
|
foreach (self::BANK_ACCOUNT_SLUGS as $slug) {
|
||||||
|
$this->syncPaymentConfiguration($slug);
|
||||||
|
}
|
||||||
|
|
||||||
$response->success = true;
|
$response->success = true;
|
||||||
$response->message = 'IBAN-Informationen wurden gespeichert.';
|
$response->message = 'IBAN-Informationen wurden gespeichert.';
|
||||||
@@ -31,15 +39,13 @@ class UpdateTenantPaymentAction
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Übernimmt die eingegebenen IBAN-Daten direkt in die Tenant-Config der Überweisungs-Zahlungsart
|
* Übernimmt die eingegebenen IBAN-Daten direkt in die Tenant-Config der Zahlungsart
|
||||||
* (Kontoinhaber/IBAN/BIC), damit sie nicht doppelt gepflegt werden müssen. Bestehende weitere
|
* (Kontoinhaber/IBAN/BIC), damit sie nicht doppelt gepflegt werden müssen. Bestehende weitere
|
||||||
* Config-Werte (z.B. Symbol) bleiben erhalten. Der Ziel-Tenant kann ein verwalteter sein, daher
|
* Config-Werte (z.B. Symbol, Gläubiger-ID) bleiben erhalten. Der Ziel-Tenant kann ein verwalteter
|
||||||
* ohne SiteScope explizit auf den Tenant-Slug gefiltert.
|
* sein, daher ohne SiteScope explizit auf den Tenant-Slug gefiltert.
|
||||||
*/
|
*/
|
||||||
private function syncTransferPaymentConfiguration(): void
|
private function syncPaymentConfiguration(string $slug): void
|
||||||
{
|
{
|
||||||
$slug = PaymentMethod::PAYMENT_ACCOUNT_TRANSACTION;
|
|
||||||
|
|
||||||
$method = AvailablePaymentMethod::withoutGlobalScope(SiteScope::class)
|
$method = AvailablePaymentMethod::withoutGlobalScope(SiteScope::class)
|
||||||
->where('tenant', $this->request->tenant->slug)
|
->where('tenant', $this->request->tenant->slug)
|
||||||
->where('slug', $slug)
|
->where('slug', $slug)
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Admin\Controllers;
|
||||||
|
|
||||||
|
use App\Enumerations\ParticipantGroupType;
|
||||||
|
use App\Scopes\CommonController;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class GlobalSettingsGetController extends CommonController
|
||||||
|
{
|
||||||
|
public function __invoke(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
return response()->json([
|
||||||
|
'groupLabels' => ParticipantGroupType::labels(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Admin\Controllers;
|
||||||
|
|
||||||
|
use App\Providers\InertiaProvider;
|
||||||
|
use App\Scopes\CommonController;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Inertia\Response;
|
||||||
|
|
||||||
|
class GlobalSettingsPageController extends CommonController
|
||||||
|
{
|
||||||
|
public function __invoke(Request $request): Response
|
||||||
|
{
|
||||||
|
return new InertiaProvider('Admin/GlobalSettings', [])->render();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Admin\Controllers;
|
||||||
|
|
||||||
|
use App\Domains\Admin\Actions\UpdateGlobalSettings\UpdateGlobalSettingsAction;
|
||||||
|
use App\Domains\Admin\Actions\UpdateGlobalSettings\UpdateGlobalSettingsRequest;
|
||||||
|
use App\Scopes\CommonController;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class GlobalSettingsUpdateController extends CommonController
|
||||||
|
{
|
||||||
|
public function __invoke(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$response = new UpdateGlobalSettingsAction(new UpdateGlobalSettingsRequest(
|
||||||
|
globalSettings: $this->globalSettings,
|
||||||
|
localGroupLabel: (string) $request->input('localGroup'),
|
||||||
|
localGroupLabelPlural: (string) $request->input('localGroupPlural'),
|
||||||
|
parentGroupLabel: (string) $request->input('parentGroup'),
|
||||||
|
parentGroupLabelPlural: (string) $request->input('parentGroupPlural'),
|
||||||
|
))->execute();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'status' => $response->success ? 'success' : 'error',
|
||||||
|
'message' => $response->message,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Admin\Controllers;
|
||||||
|
|
||||||
|
use App\Domains\Admin\Actions\CreateParentGroup\CreateParentGroupAction;
|
||||||
|
use App\Domains\Admin\Actions\CreateParentGroup\CreateParentGroupRequest;
|
||||||
|
use App\Scopes\CommonController;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class ParentGroupCreateController extends CommonController
|
||||||
|
{
|
||||||
|
public function __invoke(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$response = new CreateParentGroupAction(new CreateParentGroupRequest(
|
||||||
|
name: (string) $request->input('name'),
|
||||||
|
))->execute();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'status' => $response->success ? 'success' : 'error',
|
||||||
|
'message' => $response->message,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Admin\Controllers;
|
||||||
|
|
||||||
|
use App\Domains\Admin\Actions\DeleteParentGroup\DeleteParentGroupAction;
|
||||||
|
use App\Domains\Admin\Actions\DeleteParentGroup\DeleteParentGroupRequest;
|
||||||
|
use App\Scopes\CommonController;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class ParentGroupDeleteController extends CommonController
|
||||||
|
{
|
||||||
|
public function __invoke(int $id, Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$parentGroup = $this->adminParentGroups->findById($id);
|
||||||
|
|
||||||
|
if ($parentGroup === null) {
|
||||||
|
return response()->json(['status' => 'error', 'message' => 'Eintrag nicht gefunden.'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = new DeleteParentGroupAction(new DeleteParentGroupRequest(
|
||||||
|
parentGroup: $parentGroup,
|
||||||
|
inUse: $this->adminParentGroups->isInUse($parentGroup),
|
||||||
|
))->execute();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'status' => $response->success ? 'success' : 'error',
|
||||||
|
'message' => $response->message,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Admin\Controllers;
|
||||||
|
|
||||||
|
use App\Scopes\CommonController;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class ParentGroupListApiController extends CommonController
|
||||||
|
{
|
||||||
|
public function __invoke(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$parentGroups = $this->adminParentGroups->all()->map(fn ($parentGroup) => [
|
||||||
|
'id' => $parentGroup->id,
|
||||||
|
'name' => $parentGroup->name,
|
||||||
|
'isFallback' => $parentGroup->is_fallback,
|
||||||
|
'tenantCount' => $parentGroup->tenants_count,
|
||||||
|
'deletable' => !$parentGroup->is_fallback && !$this->adminParentGroups->isInUse($parentGroup),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json(['parentGroups' => $parentGroups]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Admin\Controllers;
|
||||||
|
|
||||||
|
use App\Enumerations\ParticipantGroupType;
|
||||||
|
use App\Providers\InertiaProvider;
|
||||||
|
use App\Scopes\CommonController;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Inertia\Response;
|
||||||
|
|
||||||
|
class ParentGroupListPageController extends CommonController
|
||||||
|
{
|
||||||
|
public function __invoke(Request $request): Response
|
||||||
|
{
|
||||||
|
return new InertiaProvider('Admin/ParentGroupList', [
|
||||||
|
'groupLabels' => ParticipantGroupType::labels(),
|
||||||
|
])->render();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Admin\Controllers;
|
||||||
|
|
||||||
|
use App\Domains\Admin\Actions\RenameParentGroup\RenameParentGroupAction;
|
||||||
|
use App\Domains\Admin\Actions\RenameParentGroup\RenameParentGroupRequest;
|
||||||
|
use App\Scopes\CommonController;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class ParentGroupUpdateController extends CommonController
|
||||||
|
{
|
||||||
|
public function __invoke(int $id, Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$parentGroup = $this->adminParentGroups->findById($id);
|
||||||
|
|
||||||
|
if ($parentGroup === null) {
|
||||||
|
return response()->json(['status' => 'error', 'message' => 'Eintrag nicht gefunden.'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = new RenameParentGroupAction(new RenameParentGroupRequest(
|
||||||
|
parentGroup: $parentGroup,
|
||||||
|
name: (string) $request->input('name'),
|
||||||
|
))->execute();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'status' => $response->success ? 'success' : 'error',
|
||||||
|
'message' => $response->message,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ class TenantCreateController extends CommonController
|
|||||||
name: $request->input('name'),
|
name: $request->input('name'),
|
||||||
slug: $request->input('slug'),
|
slug: $request->input('slug'),
|
||||||
url: $request->input('url'),
|
url: $request->input('url'),
|
||||||
|
parentGroup: $this->adminParentGroups->findById((int) $request->input('parent_group_id')),
|
||||||
));
|
));
|
||||||
|
|
||||||
$response = $action->execute();
|
$response = $action->execute();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Domains\Admin\Controllers;
|
namespace App\Domains\Admin\Controllers;
|
||||||
|
|
||||||
|
use App\Enumerations\ParticipantGroupType;
|
||||||
use App\Scopes\CommonController;
|
use App\Scopes\CommonController;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -17,6 +18,12 @@ class TenantGeneralGetController extends CommonController
|
|||||||
'slug' => $tenant->slug,
|
'slug' => $tenant->slug,
|
||||||
'url' => $tenant->url,
|
'url' => $tenant->url,
|
||||||
'is_active_local_group' => $tenant->is_active_local_group,
|
'is_active_local_group' => $tenant->is_active_local_group,
|
||||||
|
'parent_group_id' => $tenant->parent_group_id,
|
||||||
|
'parentGroups' => $this->adminParentGroups->all()->map(fn ($parentGroup) => [
|
||||||
|
'id' => $parentGroup->id,
|
||||||
|
'name' => $parentGroup->name,
|
||||||
|
]),
|
||||||
|
'parentGroupLabel' => ParticipantGroupType::ParentGroup->label(),
|
||||||
'saveEndpoint' => '/api/v1/admin/tenants/' . $slug . '/general',
|
'saveEndpoint' => '/api/v1/admin/tenants/' . $slug . '/general',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class TenantGeneralUpdateController extends CommonController
|
|||||||
name: $request->input('name'),
|
name: $request->input('name'),
|
||||||
slug: $request->input('slug'),
|
slug: $request->input('slug'),
|
||||||
url: $request->input('url'),
|
url: $request->input('url'),
|
||||||
|
parentGroup: $this->adminParentGroups->findById((int) $request->input('parent_group_id')),
|
||||||
));
|
));
|
||||||
|
|
||||||
$response = $action->execute();
|
$response = $action->execute();
|
||||||
|
|||||||
@@ -18,9 +18,17 @@ class TenantListApiController extends CommonController
|
|||||||
'slug' => $tenant->slug,
|
'slug' => $tenant->slug,
|
||||||
'url' => $tenant->url,
|
'url' => $tenant->url,
|
||||||
'is_active_local_group' => $tenant->is_active_local_group,
|
'is_active_local_group' => $tenant->is_active_local_group,
|
||||||
|
'parentGroup' => $tenant->parentGroup?->name,
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
return response()->json(['tenants' => $mapped]);
|
// Auswahl für das Anlage-Formular (Pflichtangabe).
|
||||||
|
$parentGroups = $this->adminParentGroups->all()->map(fn ($parentGroup) => [
|
||||||
|
'id' => $parentGroup->id,
|
||||||
|
'name' => $parentGroup->name,
|
||||||
|
'isFallback' => $parentGroup->is_fallback,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json(['tenants' => $mapped, 'parentGroups' => $parentGroups]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Domains\Admin\Controllers;
|
namespace App\Domains\Admin\Controllers;
|
||||||
|
|
||||||
|
use App\Enumerations\ParticipantGroupType;
|
||||||
use App\Providers\InertiaProvider;
|
use App\Providers\InertiaProvider;
|
||||||
use App\Scopes\CommonController;
|
use App\Scopes\CommonController;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -11,7 +12,9 @@ class TenantListPageController extends CommonController
|
|||||||
{
|
{
|
||||||
public function __invoke(Request $request): Response
|
public function __invoke(Request $request): Response
|
||||||
{
|
{
|
||||||
$inertiaProvider = new InertiaProvider('Admin/TenantList', []);
|
$inertiaProvider = new InertiaProvider('Admin/TenantList', [
|
||||||
|
'groupLabels' => ParticipantGroupType::labels(),
|
||||||
|
]);
|
||||||
return $inertiaProvider->render();
|
return $inertiaProvider->render();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Admin\Repositories;
|
||||||
|
|
||||||
|
use App\Models\ParentGroup;
|
||||||
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
|
||||||
|
class AdminParentGroupRepository
|
||||||
|
{
|
||||||
|
/** Alle Parent Groups, alphabetisch mit „Ohne Zuordnung" am Ende, mit der Zahl zugeordneter Mandanten. */
|
||||||
|
public function all(): Collection
|
||||||
|
{
|
||||||
|
return ParentGroup::withCount('tenants')
|
||||||
|
->ordered()
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findById(int $id): ?ParentGroup
|
||||||
|
{
|
||||||
|
return ParentGroup::find($id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ob die Parent Group noch verwendet wird -- von einem Mandanten oder von einer Anmeldung. Anmeldungen sind
|
||||||
|
* mandantengebunden; gezählt wird hier bewusst über alle Mandanten hinweg.
|
||||||
|
*/
|
||||||
|
public function isInUse(ParentGroup $parentGroup): bool
|
||||||
|
{
|
||||||
|
return $parentGroup->tenants()->exists()
|
||||||
|
|| $parentGroup->eventParticipants()->withoutGlobalScopes()->exists();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ use App\Domains\Admin\Controllers\DocumentAssetUpdateController;
|
|||||||
use App\Domains\Admin\Controllers\DocumentTemplatesGetController;
|
use App\Domains\Admin\Controllers\DocumentTemplatesGetController;
|
||||||
use App\Domains\Admin\Controllers\DocumentTemplatesPreviewController;
|
use App\Domains\Admin\Controllers\DocumentTemplatesPreviewController;
|
||||||
use App\Domains\Admin\Controllers\DocumentTemplatesUpdateController;
|
use App\Domains\Admin\Controllers\DocumentTemplatesUpdateController;
|
||||||
|
use App\Domains\Admin\Controllers\GlobalSettingsGetController;
|
||||||
|
use App\Domains\Admin\Controllers\GlobalSettingsUpdateController;
|
||||||
use App\Domains\Admin\Controllers\ManagedTenantContactGetController;
|
use App\Domains\Admin\Controllers\ManagedTenantContactGetController;
|
||||||
use App\Domains\Admin\Controllers\ManagedTenantContactUpdateController;
|
use App\Domains\Admin\Controllers\ManagedTenantContactUpdateController;
|
||||||
use App\Domains\Admin\Controllers\ManagedTenantGdprGetController;
|
use App\Domains\Admin\Controllers\ManagedTenantGdprGetController;
|
||||||
@@ -15,6 +17,10 @@ use App\Domains\Admin\Controllers\ManagedTenantPaymentGetController;
|
|||||||
use App\Domains\Admin\Controllers\ManagedTenantPaymentUpdateController;
|
use App\Domains\Admin\Controllers\ManagedTenantPaymentUpdateController;
|
||||||
use App\Domains\Admin\Controllers\ManagedTenantTaxGetController;
|
use App\Domains\Admin\Controllers\ManagedTenantTaxGetController;
|
||||||
use App\Domains\Admin\Controllers\ManagedTenantTaxUpdateController;
|
use App\Domains\Admin\Controllers\ManagedTenantTaxUpdateController;
|
||||||
|
use App\Domains\Admin\Controllers\ParentGroupCreateController;
|
||||||
|
use App\Domains\Admin\Controllers\ParentGroupDeleteController;
|
||||||
|
use App\Domains\Admin\Controllers\ParentGroupListApiController;
|
||||||
|
use App\Domains\Admin\Controllers\ParentGroupUpdateController;
|
||||||
use App\Domains\Admin\Controllers\TenantContactGetController;
|
use App\Domains\Admin\Controllers\TenantContactGetController;
|
||||||
use App\Domains\Admin\Controllers\TenantContactUpdateController;
|
use App\Domains\Admin\Controllers\TenantContactUpdateController;
|
||||||
use App\Domains\Admin\Controllers\TenantCreateController;
|
use App\Domains\Admin\Controllers\TenantCreateController;
|
||||||
@@ -76,6 +82,12 @@ Route::middleware([IdentifyTenant::class, 'auth', AdminRoleMiddleware::class])->
|
|||||||
Route::post('/assets', DocumentAssetUpdateController::class);
|
Route::post('/assets', DocumentAssetUpdateController::class);
|
||||||
Route::delete('/assets/{name}', DocumentAssetDeleteController::class);
|
Route::delete('/assets/{name}', DocumentAssetDeleteController::class);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Bezeichnungen der Gruppen-Ebenen gelten ebenfalls app-weit.
|
||||||
|
Route::prefix('api/v1/admin/settings')->group(function () {
|
||||||
|
Route::get('/', GlobalSettingsGetController::class);
|
||||||
|
Route::post('/', GlobalSettingsUpdateController::class);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::middleware(LvOnlyMiddleware::class)->group(function () {
|
Route::middleware(LvOnlyMiddleware::class)->group(function () {
|
||||||
@@ -97,5 +109,12 @@ Route::middleware([IdentifyTenant::class, 'auth', AdminRoleMiddleware::class])->
|
|||||||
Route::post('/tax', ManagedTenantTaxUpdateController::class);
|
Route::post('/tax', ManagedTenantTaxUpdateController::class);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Route::prefix('api/v1/admin/parent-groups')->group(function () {
|
||||||
|
Route::get('/list', ParentGroupListApiController::class);
|
||||||
|
Route::post('/create', ParentGroupCreateController::class);
|
||||||
|
Route::post('/{id}', ParentGroupUpdateController::class)->whereNumber('id');
|
||||||
|
Route::delete('/{id}', ParentGroupDeleteController::class)->whereNumber('id');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
use App\Domains\Admin\Controllers\AdminDashboardController;
|
use App\Domains\Admin\Controllers\AdminDashboardController;
|
||||||
use App\Domains\Admin\Controllers\DocumentTemplatesPageController;
|
use App\Domains\Admin\Controllers\DocumentTemplatesPageController;
|
||||||
|
use App\Domains\Admin\Controllers\GlobalSettingsPageController;
|
||||||
|
use App\Domains\Admin\Controllers\ParentGroupListPageController;
|
||||||
use App\Domains\Admin\Controllers\TenantEditPageController;
|
use App\Domains\Admin\Controllers\TenantEditPageController;
|
||||||
use App\Domains\Admin\Controllers\TenantListPageController;
|
use App\Domains\Admin\Controllers\TenantListPageController;
|
||||||
use App\Domains\Admin\Controllers\TenantPageController;
|
use App\Domains\Admin\Controllers\TenantPageController;
|
||||||
@@ -21,11 +23,13 @@ Route::middleware([IdentifyTenant::class, 'auth', AdminRoleMiddleware::class])->
|
|||||||
// App-weit gültige Dokumentvorlagen -- nur für Hauptadministrator*innen.
|
// App-weit gültige Dokumentvorlagen -- nur für Hauptadministrator*innen.
|
||||||
Route::middleware(MainAdminRoleMiddleware::class)->group(function () {
|
Route::middleware(MainAdminRoleMiddleware::class)->group(function () {
|
||||||
Route::get('/document-templates', DocumentTemplatesPageController::class);
|
Route::get('/document-templates', DocumentTemplatesPageController::class);
|
||||||
|
Route::get('/settings', GlobalSettingsPageController::class);
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::middleware(LvOnlyMiddleware::class)->group(function () {
|
Route::middleware(LvOnlyMiddleware::class)->group(function () {
|
||||||
Route::get('/tenants', TenantListPageController::class);
|
Route::get('/tenants', TenantListPageController::class);
|
||||||
Route::get('/tenants/{slug}', TenantEditPageController::class);
|
Route::get('/tenants/{slug}', TenantEditPageController::class);
|
||||||
|
Route::get('/parent-groups', ParentGroupListPageController::class);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import AdminAppLayout from "../../../../resources/js/layouts/AdminAppLayout.vue";
|
||||||
|
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
|
||||||
|
import { useAjax } from "../../../../resources/js/components/ajaxHandler.js";
|
||||||
|
import { toast } from "vue3-toastify";
|
||||||
|
|
||||||
|
const { request } = useAjax()
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
localGroup: '',
|
||||||
|
localGroupPlural: '',
|
||||||
|
parentGroup: '',
|
||||||
|
parentGroupPlural: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const response = await request('/api/v1/admin/settings')
|
||||||
|
Object.assign(form.value, response?.groupLabels ?? {})
|
||||||
|
})
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
const response = await request('/api/v1/admin/settings', {
|
||||||
|
method: 'POST',
|
||||||
|
body: form.value,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response && response.status === 'success') {
|
||||||
|
toast.success(response.message)
|
||||||
|
} else {
|
||||||
|
toast.error(response?.message ?? 'Fehler beim Speichern')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AdminAppLayout title="Einstellungen">
|
||||||
|
<shadowed-box style="width: 95%; margin: 20px auto; padding: 20px; overflow-x: hidden;">
|
||||||
|
<h3>Bezeichnungen der Gruppen</h3>
|
||||||
|
<p class="hint">
|
||||||
|
Gelten für alle Mandanten: in Anmeldungen, Teilnehmendenlisten, Exporten und in der Verwaltung.
|
||||||
|
</p>
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>Einzahl</th>
|
||||||
|
<th>Mehrzahl</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>Gruppe (Mandant)</th>
|
||||||
|
<td><input type="text" v-model="form.localGroup" class="form-input" placeholder="z. B. Stamm" /></td>
|
||||||
|
<td><input type="text" v-model="form.localGroupPlural" class="form-input" placeholder="z. B. Stämme" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Übergeordnete Gruppe</th>
|
||||||
|
<td><input type="text" v-model="form.parentGroup" class="form-input" placeholder="z. B. Bund" /></td>
|
||||||
|
<td><input type="text" v-model="form.parentGroupPlural" class="form-input" placeholder="z. B. Bünde" /></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<button class="btn-save" @click="save">Speichern</button>
|
||||||
|
</shadowed-box>
|
||||||
|
</AdminAppLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.hint {
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table th {
|
||||||
|
text-align: left;
|
||||||
|
padding: 8px 12px;
|
||||||
|
color: #374151;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table td {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-save {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding: 8px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
background-color: #16a34a;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-save:hover {
|
||||||
|
background-color: #15803d;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import AdminAppLayout from "../../../../resources/js/layouts/AdminAppLayout.vue";
|
||||||
|
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
|
||||||
|
import { useAjax } from "../../../../resources/js/components/ajaxHandler.js";
|
||||||
|
import { toast } from "vue3-toastify";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
groupLabels: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({ localGroupPlural: 'Stämme', parentGroup: 'Bund', parentGroupPlural: 'Bünde' }),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { request } = useAjax()
|
||||||
|
|
||||||
|
const parentGroups = ref([])
|
||||||
|
const newName = ref('')
|
||||||
|
const showCreate = ref(false)
|
||||||
|
// ID des Eintrags, der gerade umbenannt wird, samt Entwurf des neuen Namens.
|
||||||
|
const editingId = ref(null)
|
||||||
|
const editName = ref('')
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadParentGroups()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function loadParentGroups() {
|
||||||
|
const response = await request('/api/v1/admin/parent-groups/list')
|
||||||
|
parentGroups.value = response?.parentGroups ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createParentGroup() {
|
||||||
|
const response = await request('/api/v1/admin/parent-groups/create', {
|
||||||
|
method: 'POST',
|
||||||
|
body: { name: newName.value },
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response && response.status === 'success') {
|
||||||
|
toast.success(response.message)
|
||||||
|
newName.value = ''
|
||||||
|
showCreate.value = false
|
||||||
|
await loadParentGroups()
|
||||||
|
} else {
|
||||||
|
toast.error(response?.message ?? 'Fehler beim Anlegen')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startEditing(parentGroup) {
|
||||||
|
editingId.value = parentGroup.id
|
||||||
|
editName.value = parentGroup.name
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveName(parentGroup) {
|
||||||
|
const response = await request('/api/v1/admin/parent-groups/' + parentGroup.id, {
|
||||||
|
method: 'POST',
|
||||||
|
body: { name: editName.value },
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response && response.status === 'success') {
|
||||||
|
toast.success(response.message)
|
||||||
|
editingId.value = null
|
||||||
|
await loadParentGroups()
|
||||||
|
} else {
|
||||||
|
toast.error(response?.message ?? 'Fehler beim Speichern')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteParentGroup(parentGroup) {
|
||||||
|
if (!confirm('„' + parentGroup.name + '" wirklich löschen?')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await request('/api/v1/admin/parent-groups/' + parentGroup.id, {
|
||||||
|
method: 'DELETE',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response && response.status === 'success') {
|
||||||
|
toast.success(response.message)
|
||||||
|
await loadParentGroups()
|
||||||
|
} else {
|
||||||
|
toast.error(response?.message ?? 'Fehler beim Löschen')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AdminAppLayout :title="props.groupLabels.parentGroupPlural">
|
||||||
|
<shadowed-box style="width: 95%; margin: 20px auto; padding: 20px; overflow-x: hidden;">
|
||||||
|
<table class="parent-group-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Zugeordnete {{ props.groupLabels.localGroupPlural }}</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="parentGroup in parentGroups" :key="parentGroup.id">
|
||||||
|
<td>
|
||||||
|
<input v-if="editingId === parentGroup.id" type="text" v-model="editName" class="form-input"
|
||||||
|
@keyup.enter="saveName(parentGroup)" />
|
||||||
|
<span v-else>{{ parentGroup.name }}</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ parentGroup.tenantCount }}</td>
|
||||||
|
<td class="actions">
|
||||||
|
<template v-if="editingId === parentGroup.id">
|
||||||
|
<button class="btn-save" @click="saveName(parentGroup)">Speichern</button>
|
||||||
|
<button class="btn-cancel" @click="editingId = null">Abbrechen</button>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<button class="btn-edit" @click="startEditing(parentGroup)">Umbenennen</button>
|
||||||
|
<button v-if="parentGroup.deletable" class="btn-delete" @click="deleteParentGroup(parentGroup)">Löschen</button>
|
||||||
|
</template>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p class="hint">
|
||||||
|
Löschen ist nur möglich, solange keine {{ props.groupLabels.localGroupPlural }} und keine Anmeldungen
|
||||||
|
zugeordnet sind. „Ohne Zuordnung" bleibt immer bestehen.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style="margin-top: 20px;">
|
||||||
|
<button v-if="!showCreate" class="btn-create" @click="showCreate = true">Neuen Eintrag anlegen</button>
|
||||||
|
|
||||||
|
<div v-if="showCreate" class="create-form">
|
||||||
|
<h3>Neuen Eintrag anlegen ({{ props.groupLabels.parentGroup }})</h3>
|
||||||
|
<input type="text" v-model="newName" class="form-input" placeholder="Name"
|
||||||
|
@keyup.enter="createParentGroup" />
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn-save" @click="createParentGroup">Anlegen</button>
|
||||||
|
<button class="btn-cancel" @click="showCreate = false">Abbrechen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</shadowed-box>
|
||||||
|
</AdminAppLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.parent-group-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parent-group-table thead th {
|
||||||
|
text-align: left;
|
||||||
|
padding: 10px 16px;
|
||||||
|
background-color: #f9fafb;
|
||||||
|
color: #374151;
|
||||||
|
font-weight: bold;
|
||||||
|
border-bottom: 2px solid #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parent-group-table tbody td {
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
margin-top: 12px;
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-form {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-form h3 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-create, .btn-save, .btn-cancel, .btn-edit, .btn-delete {
|
||||||
|
padding: 6px 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-create, .btn-edit {
|
||||||
|
background-color: #1d4899;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-create:hover, .btn-edit:hover {
|
||||||
|
background-color: #163a7a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-save {
|
||||||
|
background-color: #16a34a;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-save:hover {
|
||||||
|
background-color: #15803d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel {
|
||||||
|
background-color: #e5e7eb;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel:hover {
|
||||||
|
background-color: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-delete {
|
||||||
|
background-color: #dc2626;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-delete:hover {
|
||||||
|
background-color: #b91c1c;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { useAjax } from "../../../../../resources/js/components/ajaxHandler.js";
|
import { useAjax } from "../../../../../resources/js/components/ajaxHandler.js";
|
||||||
import { toast } from "vue3-toastify";
|
import { toast } from "vue3-toastify";
|
||||||
|
|
||||||
@@ -17,8 +17,16 @@ const form = ref({
|
|||||||
name: props.data.name ?? '',
|
name: props.data.name ?? '',
|
||||||
slug: props.data.slug ?? '',
|
slug: props.data.slug ?? '',
|
||||||
url: props.data.url ?? '',
|
url: props.data.url ?? '',
|
||||||
|
parent_group_id: props.data.parent_group_id ?? null,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Übergeordnete Gruppe (z. B. „Bund") -- Pflichtangabe, Auswahl aus der LV-seitig gepflegten Liste.
|
||||||
|
const parentGroups = props.data.parentGroups ?? []
|
||||||
|
const parentGroupLabel = props.data.parentGroupLabel ?? 'Bund'
|
||||||
|
const parentGroupName = computed(() =>
|
||||||
|
parentGroups.find(g => g.id === form.value.parent_group_id)?.name ?? '—'
|
||||||
|
)
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
const saveUrl = props.data.saveEndpoint ?? '/api/v1/admin/tenants/' + props.data.slug + '/general'
|
const saveUrl = props.data.saveEndpoint ?? '/api/v1/admin/tenants/' + props.data.slug + '/general'
|
||||||
const response = await request(saveUrl, {
|
const response = await request(saveUrl, {
|
||||||
@@ -39,6 +47,7 @@ async function save() {
|
|||||||
<div v-if="!editing">
|
<div v-if="!editing">
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
<tr><th>Name:</th><td>{{ form.name }}</td></tr>
|
<tr><th>Name:</th><td>{{ form.name }}</td></tr>
|
||||||
|
<tr><th>{{ parentGroupLabel }}:</th><td>{{ parentGroupName }}</td></tr>
|
||||||
<tr><th>Slug:</th><td>{{ form.slug }}</td></tr>
|
<tr><th>Slug:</th><td>{{ form.slug }}</td></tr>
|
||||||
<tr><th>URL:</th><td>{{ form.url }}</td></tr>
|
<tr><th>URL:</th><td>{{ form.url }}</td></tr>
|
||||||
<tr><th>Status:</th><td>
|
<tr><th>Status:</th><td>
|
||||||
@@ -54,6 +63,14 @@ async function save() {
|
|||||||
<th>Name:</th>
|
<th>Name:</th>
|
||||||
<td><input type="text" v-model="form.name" class="form-input" /></td>
|
<td><input type="text" v-model="form.name" class="form-input" /></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{{ parentGroupLabel }}:</th>
|
||||||
|
<td>
|
||||||
|
<select v-model="form.parent_group_id" class="form-input">
|
||||||
|
<option v-for="group in parentGroups" :key="group.id" :value="group.id">{{ group.name }}</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Slug:</th>
|
<th>Slug:</th>
|
||||||
<td><input type="text" v-model="form.slug" class="form-input" /></td>
|
<td><input type="text" v-model="form.slug" class="form-input" /></td>
|
||||||
|
|||||||
@@ -107,6 +107,8 @@ async function save() {
|
|||||||
v-model="form.configuration[option.name]"
|
v-model="form.configuration[option.name]"
|
||||||
:defaults="data.statementRulesetDefault ?? {}"
|
:defaults="data.statementRulesetDefault ?? {}"
|
||||||
:charsets="data.statementRulesetCharsets ?? undefined"/>
|
:charsets="data.statementRulesetCharsets ?? undefined"/>
|
||||||
|
<input v-else-if="option.type === 'number'" type="number" step="1"
|
||||||
|
v-model="form.configuration[option.name]" class="form-input"/>
|
||||||
<input v-else type="text" v-model="form.configuration[option.name]" class="form-input"/>
|
<input v-else type="text" v-model="form.configuration[option.name]" class="form-input"/>
|
||||||
<span v-if="option.hint" class="option-hint">{{ option.hint }}</span>
|
<span v-if="option.hint" class="option-hint">{{ option.hint }}</span>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -5,6 +5,13 @@ import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
|
|||||||
import { useAjax } from "../../../../resources/js/components/ajaxHandler.js";
|
import { useAjax } from "../../../../resources/js/components/ajaxHandler.js";
|
||||||
import { toast } from "vue3-toastify";
|
import { toast } from "vue3-toastify";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
groupLabels: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({ localGroup: 'Stamm', localGroupPlural: 'Stämme', parentGroup: 'Bund' }),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const { request } = useAjax()
|
const { request } = useAjax()
|
||||||
|
|
||||||
function openTenant(slug) {
|
function openTenant(slug) {
|
||||||
@@ -12,12 +19,16 @@ function openTenant(slug) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tenants = ref([])
|
const tenants = ref([])
|
||||||
|
const parentGroups = ref([])
|
||||||
const showCreate = ref(false)
|
const showCreate = ref(false)
|
||||||
const createForm = ref({
|
const emptyForm = () => ({
|
||||||
name: '',
|
name: '',
|
||||||
slug: '',
|
slug: '',
|
||||||
url: '',
|
url: '',
|
||||||
|
// Pflichtangabe; vorbelegt mit „Ohne Zuordnung", sobald die Liste geladen ist.
|
||||||
|
parent_group_id: parentGroups.value.find(g => g.isFallback)?.id ?? null,
|
||||||
})
|
})
|
||||||
|
const createForm = ref(emptyForm())
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadTenants()
|
await loadTenants()
|
||||||
@@ -33,6 +44,11 @@ async function loadTenants() {
|
|||||||
})
|
})
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
tenants.value = data.tenants
|
tenants.value = data.tenants
|
||||||
|
parentGroups.value = data.parentGroups ?? []
|
||||||
|
|
||||||
|
if (createForm.value.parent_group_id === null) {
|
||||||
|
createForm.value.parent_group_id = parentGroups.value.find(g => g.isFallback)?.id ?? null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createTenant() {
|
async function createTenant() {
|
||||||
@@ -44,7 +60,7 @@ async function createTenant() {
|
|||||||
if (response && response.status === 'success') {
|
if (response && response.status === 'success') {
|
||||||
toast.success(response.message)
|
toast.success(response.message)
|
||||||
showCreate.value = false
|
showCreate.value = false
|
||||||
createForm.value = { name: '', slug: '', url: '' }
|
createForm.value = emptyForm()
|
||||||
await loadTenants()
|
await loadTenants()
|
||||||
} else {
|
} else {
|
||||||
toast.error(response?.message ?? 'Fehler beim Anlegen')
|
toast.error(response?.message ?? 'Fehler beim Anlegen')
|
||||||
@@ -53,12 +69,13 @@ async function createTenant() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AdminAppLayout title="Stämme">
|
<AdminAppLayout :title="props.groupLabels.localGroupPlural">
|
||||||
<shadowed-box style="width: 95%; margin: 20px auto; padding: 20px; overflow-x: hidden;">
|
<shadowed-box style="width: 95%; margin: 20px auto; padding: 20px; overflow-x: hidden;">
|
||||||
<table class="tenant-table">
|
<table class="tenant-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
|
<th>{{ props.groupLabels.parentGroup }}</th>
|
||||||
<th>Slug</th>
|
<th>Slug</th>
|
||||||
<th>URL</th>
|
<th>URL</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
@@ -67,6 +84,7 @@ async function createTenant() {
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="tenant in tenants" :key="tenant.slug" class="tenant-row" @click="openTenant(tenant.slug)">
|
<tr v-for="tenant in tenants" :key="tenant.slug" class="tenant-row" @click="openTenant(tenant.slug)">
|
||||||
<td>{{ tenant.name }}</td>
|
<td>{{ tenant.name }}</td>
|
||||||
|
<td>{{ tenant.parentGroup ?? '—' }}</td>
|
||||||
<td>{{ tenant.slug }}</td>
|
<td>{{ tenant.slug }}</td>
|
||||||
<td>{{ tenant.url }}</td>
|
<td>{{ tenant.url }}</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -79,15 +97,23 @@ async function createTenant() {
|
|||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div style="margin-top: 20px;">
|
<div style="margin-top: 20px;">
|
||||||
<button v-if="!showCreate" class="btn-create" @click="showCreate = true">Neuen Stamm anlegen</button>
|
<button v-if="!showCreate" class="btn-create" @click="showCreate = true">Neuen Eintrag anlegen ({{ props.groupLabels.localGroup }})</button>
|
||||||
|
|
||||||
<div v-if="showCreate" class="create-form">
|
<div v-if="showCreate" class="create-form">
|
||||||
<h3>Neuen Stamm anlegen</h3>
|
<h3>Neuen Eintrag anlegen ({{ props.groupLabels.localGroup }})</h3>
|
||||||
<table class="form-table">
|
<table class="form-table">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<td><input type="text" v-model="createForm.name" class="form-input" /></td>
|
<td><input type="text" v-model="createForm.name" class="form-input" /></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{{ props.groupLabels.parentGroup }}</th>
|
||||||
|
<td>
|
||||||
|
<select v-model="createForm.parent_group_id" class="form-input">
|
||||||
|
<option v-for="group in parentGroups" :key="group.id" :value="group.id">{{ group.name }}</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Slug</th>
|
<th>Slug</th>
|
||||||
<td><input type="text" v-model="createForm.slug" class="form-input" /></td>
|
<td><input type="text" v-model="createForm.slug" class="form-input" /></td>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class ChangeCostUnitDetailsCommand {
|
|||||||
|
|
||||||
$this->request->costUnit->distance_allowance = $this->request->distanceAllowance->getAmount();
|
$this->request->costUnit->distance_allowance = $this->request->distanceAllowance->getAmount();
|
||||||
$this->request->costUnit->mail_on_new = $this->request->mailOnNew;
|
$this->request->costUnit->mail_on_new = $this->request->mailOnNew;
|
||||||
$this->request->costUnit->billing_deadline = $this->request->billingDeadline;
|
$this->request->costUnit->billing_deadline = $this->request->billingDeadline?->format('Y-m-d');
|
||||||
|
|
||||||
$response->success = $this->request->costUnit->save();
|
$response->success = $this->request->costUnit->save();
|
||||||
return $response;
|
return $response;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class CreateCostUnitCommand {
|
|||||||
'name' => $this->request->name,
|
'name' => $this->request->name,
|
||||||
'tenant' => currentTenant()->slug,
|
'tenant' => currentTenant()->slug,
|
||||||
'type' => $this->request->type,
|
'type' => $this->request->type,
|
||||||
'billing_deadline' => $this->request->billingDeadline,
|
'billing_deadline' => $this->request->billingDeadline?->format('Y-m-d'),
|
||||||
'distance_allowance' => $this->request->distanceAllowance->getAmount(),
|
'distance_allowance' => $this->request->distanceAllowance->getAmount(),
|
||||||
'mail_on_new' => $this->request->mailOnNew,
|
'mail_on_new' => $this->request->mailOnNew,
|
||||||
'allow_new' => true,
|
'allow_new' => true,
|
||||||
|
|||||||
@@ -37,7 +37,10 @@ class EditController extends CommonController{
|
|||||||
|
|
||||||
$saveParams = $request->get('formData');
|
$saveParams = $request->get('formData');
|
||||||
$distanceAllowance = Amount::fromString($saveParams['distanceAllowance']);
|
$distanceAllowance = Amount::fromString($saveParams['distanceAllowance']);
|
||||||
$billingDeadline = isset($saveParams['billingDeadline']) ? \DateTime::createFromFormat('Y-m-d', $saveParams['billingDeadline']) : null;
|
$rawBillingDeadline = $saveParams['billingDeadline'] ?? null;
|
||||||
|
$billingDeadline = !empty($rawBillingDeadline)
|
||||||
|
? (\DateTime::createFromFormat('!Y-m-d', $rawBillingDeadline) ?: null)
|
||||||
|
: null;
|
||||||
|
|
||||||
$request = new ChangeCostUnitDetailsRequest($costUnit, $distanceAllowance, $saveParams['mailOnNew'], $billingDeadline);
|
$request = new ChangeCostUnitDetailsRequest($costUnit, $distanceAllowance, $saveParams['mailOnNew'], $billingDeadline);
|
||||||
$command = new ChangeCostUnitDetailsCommand($request);
|
$command = new ChangeCostUnitDetailsCommand($request);
|
||||||
|
|||||||
+6
@@ -12,6 +12,12 @@ class CertificateOfConductionCheckCommand {
|
|||||||
public function execute() : CertificateOfConductionCheckResponse {
|
public function execute() : CertificateOfConductionCheckResponse {
|
||||||
$response = new CertificateOfConductionCheckResponse();
|
$response = new CertificateOfConductionCheckResponse();
|
||||||
|
|
||||||
|
// Verlangt die Veranstaltung kein eFZ, entfällt die Prüfung -- ohne Aufruf der Prüf-API.
|
||||||
|
if (!$this->request->participant->event()->first()?->coc_check_required) {
|
||||||
|
$response->status = EfzStatus::EFZ_STATUS_NOT_REQUIRED;
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
// Der Stamm ist optional (die Kurzanmeldung erhebt ihn nicht, in der Verwaltung kann er entfernt werden).
|
// Der Stamm ist optional (die Kurzanmeldung erhebt ihn nicht, in der Verwaltung kann er entfernt werden).
|
||||||
$localGroup = str_replace('Stamm ', '', $this->request->participant->localGroup()->first()?->name ?? '');
|
$localGroup = str_replace('Stamm ', '', $this->request->participant->localGroup()->first()?->name ?? '');
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ class CreateEventCommand {
|
|||||||
'vat_pricing_mode' => $tenant->vat_pricing_mode,
|
'vat_pricing_mode' => $tenant->vat_pricing_mode,
|
||||||
'tax_exemption_reason' => $tenant->tax_exemption_reason,
|
'tax_exemption_reason' => $tenant->tax_exemption_reason,
|
||||||
'tax_exemption_note' => $tenant->tax_exemption_note,
|
'tax_exemption_note' => $tenant->tax_exemption_note,
|
||||||
|
'participant_group_type' => $this->request->participantGroupType->value,
|
||||||
// Den Event-Teil der Rechnungsnummer einfrieren: Tenant-Präfix und Startdatum sind später
|
// Den Event-Teil der Rechnungsnummer einfrieren: Tenant-Präfix und Startdatum sind später
|
||||||
// änderbar, eine herausgegebene Rechnung würde sonst zu einer anderen Nummer gehören.
|
// änderbar, eine herausgegebene Rechnung würde sonst zu einer anderen Nummer gehören.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Domains\Event\Actions\CreateEvent;
|
namespace App\Domains\Event\Actions\CreateEvent;
|
||||||
|
|
||||||
|
use App\Enumerations\ParticipantGroupType;
|
||||||
use App\Enumerations\ParticipationFeeType;
|
use App\Enumerations\ParticipationFeeType;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
|
|
||||||
@@ -21,8 +22,11 @@ class CreateEventRequest {
|
|||||||
public bool $payPerDay;
|
public bool $payPerDay;
|
||||||
public ?string $street;
|
public ?string $street;
|
||||||
public ?string $houseNumber;
|
public ?string $houseNumber;
|
||||||
|
/** Wonach bei der Anmeldung gefragt wird: Local Group oder Parent Group. */
|
||||||
|
public ParticipantGroupType $participantGroupType;
|
||||||
|
|
||||||
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, ?string $street = null, ?string $houseNumber = null) {
|
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, ?string $street = null, ?string $houseNumber = null, ParticipantGroupType $participantGroupType = ParticipantGroupType::LocalGroup) {
|
||||||
|
$this->participantGroupType = $participantGroupType;
|
||||||
$this->name = $name;
|
$this->name = $name;
|
||||||
$this->location = $location;
|
$this->location = $location;
|
||||||
$this->postalCode = $postalCode;
|
$this->postalCode = $postalCode;
|
||||||
|
|||||||
+33
-5
@@ -5,6 +5,7 @@ namespace App\Domains\Event\Actions\CreateIncomeSurplusStatement;
|
|||||||
use App\Enumerations\ParticipationType;
|
use App\Enumerations\ParticipationType;
|
||||||
use App\Models\CostUnit;
|
use App\Models\CostUnit;
|
||||||
use App\Models\Event;
|
use App\Models\Event;
|
||||||
|
use App\Models\Invoice;
|
||||||
use App\Providers\PdfGenerateAndDownloadProvider;
|
use App\Providers\PdfGenerateAndDownloadProvider;
|
||||||
use App\Repositories\CostUnitRepository;
|
use App\Repositories\CostUnitRepository;
|
||||||
use App\ValueObjects\Amount;
|
use App\ValueObjects\Amount;
|
||||||
@@ -52,8 +53,9 @@ class CreateIncomeSurplusStatementCommand
|
|||||||
|
|
||||||
$eventData = $this->event->toResource()->toArray(new Request());
|
$eventData = $this->event->toResource()->toArray(new Request());
|
||||||
|
|
||||||
$income = $this->buildIncome($eventData, $otherIncome);
|
|
||||||
$expenses = $this->buildExpenses($costUnit);
|
$expenses = $this->buildExpenses($costUnit);
|
||||||
|
$donations = $this->buildExpenses($costUnit, donationsOnly: true);
|
||||||
|
$income = $this->buildIncome($eventData, $otherIncome, $donations['total']);
|
||||||
|
|
||||||
$result = new Amount($income['total']->getAmount() - $expenses['total']->getAmount(), 'Euro');
|
$result = new Amount($income['total']->getAmount() - $expenses['total']->getAmount(), 'Euro');
|
||||||
|
|
||||||
@@ -62,6 +64,7 @@ class CreateIncomeSurplusStatementCommand
|
|||||||
'createdAt' => new \DateTime()->format('d.m.Y'),
|
'createdAt' => new \DateTime()->format('d.m.Y'),
|
||||||
'income' => $income,
|
'income' => $income,
|
||||||
'expenses' => $expenses,
|
'expenses' => $expenses,
|
||||||
|
'donations' => $donations,
|
||||||
'result' => $result,
|
'result' => $result,
|
||||||
'money' => self::money(...),
|
'money' => self::money(...),
|
||||||
])->render();
|
])->render();
|
||||||
@@ -70,6 +73,7 @@ class CreateIncomeSurplusStatementCommand
|
|||||||
$response->filename = 'EUER-' . $this->event->identifier . '.pdf';
|
$response->filename = 'EUER-' . $this->event->identifier . '.pdf';
|
||||||
$response->income = $income;
|
$response->income = $income;
|
||||||
$response->expenses = $expenses;
|
$response->expenses = $expenses;
|
||||||
|
$response->donations = $donations;
|
||||||
$response->result = $result;
|
$response->result = $result;
|
||||||
$response->pdfContent = PdfGenerateAndDownloadProvider::fromHtml($html, 'portrait');
|
$response->pdfContent = PdfGenerateAndDownloadProvider::fromHtml($html, 'portrait');
|
||||||
|
|
||||||
@@ -87,7 +91,7 @@ class CreateIncomeSurplusStatementCommand
|
|||||||
*
|
*
|
||||||
* @return array{categories: array<int, array{name: string, total: Amount, entries: array<int, array{name: string, amount: Amount}>}>, total: Amount}
|
* @return array{categories: array<int, array{name: string, total: Amount, entries: array<int, array{name: string, amount: Amount}>}>, total: Amount}
|
||||||
*/
|
*/
|
||||||
private function buildIncome(array $eventData, float $otherIncome): array
|
private function buildIncome(array $eventData, float $otherIncome, Amount $donations): array
|
||||||
{
|
{
|
||||||
// Beiträge aller Teilnahmearten in einer Zeile: Für die Mittelverwendung zählt, was an Beiträgen
|
// Beiträge aller Teilnahmearten in einer Zeile: Für die Mittelverwendung zählt, was an Beiträgen
|
||||||
// hereingekommen ist, nicht von wem.
|
// hereingekommen ist, nicht von wem.
|
||||||
@@ -118,6 +122,8 @@ class CreateIncomeSurplusStatementCommand
|
|||||||
'name' => 'Fördermittel (' . self::money($supportRate) . ' € p.P./Tag)',
|
'name' => 'Fördermittel (' . self::money($supportRate) . ' € p.P./Tag)',
|
||||||
'amount' => new Amount($eventData['supportPerson']['amount']->getAmount(), 'Euro'),
|
'amount' => new Amount($eventData['supportPerson']['amount']->getAmount(), 'Euro'),
|
||||||
],
|
],
|
||||||
|
// Gespendete Auslagen: als Ausgabe ihres Typs gebucht, hier die Gegenbuchung.
|
||||||
|
['name' => 'Spenden', 'amount' => $donations],
|
||||||
];
|
];
|
||||||
|
|
||||||
$categories = [
|
$categories = [
|
||||||
@@ -134,14 +140,17 @@ class CreateIncomeSurplusStatementCommand
|
|||||||
/**
|
/**
|
||||||
* Die Ausgabenseite: eine Zeile je Ausgabentyp, dazu die Belege für die Anlage.
|
* Die Ausgabenseite: eine Zeile je Ausgabentyp, dazu die Belege für die Anlage.
|
||||||
*
|
*
|
||||||
|
* Mit `$donationsOnly` nur die gespendeten Auslagen in derselben Gliederung -- für die Anlage „Spenden"
|
||||||
|
* und die Summe unter den Förderungen.
|
||||||
|
*
|
||||||
* @return array{groups: array<int, array{name: string, sum: Amount, rows: array<int, array{number: string, date: string, purpose: string, amount: Amount}>}>, total: Amount}
|
* @return array{groups: array<int, array{name: string, sum: Amount, rows: array<int, array{number: string, date: string, purpose: string, amount: Amount}>}>, total: Amount}
|
||||||
*/
|
*/
|
||||||
private function buildExpenses(CostUnit $costUnit): array
|
private function buildExpenses(CostUnit $costUnit, bool $donationsOnly = false): array
|
||||||
{
|
{
|
||||||
$groups = [];
|
$groups = [];
|
||||||
$total = new Amount(0, 'Euro');
|
$total = new Amount(0, 'Euro');
|
||||||
|
|
||||||
foreach ($this->costUnits->groupExpensesByType($costUnit) as $group) {
|
foreach ($this->costUnits->groupExpensesByType($costUnit, $donationsOnly) as $group) {
|
||||||
$rows = [];
|
$rows = [];
|
||||||
|
|
||||||
foreach ($group['invoices'] as $invoice) {
|
foreach ($group['invoices'] as $invoice) {
|
||||||
@@ -150,7 +159,7 @@ class CreateIncomeSurplusStatementCommand
|
|||||||
'date' => $invoice->created_at?->format('d.m.Y') ?? '',
|
'date' => $invoice->created_at?->format('d.m.Y') ?? '',
|
||||||
// Ohne die Anmerkung: dort steht, was die Kassenwart*in beim Korrigieren notiert hat,
|
// Ohne die Anmerkung: dort steht, was die Kassenwart*in beim Korrigieren notiert hat,
|
||||||
// und das gehört auf den Beleg, nicht in den Zweck.
|
// und das gehört auf den Beleg, nicht in den Zweck.
|
||||||
'purpose' => $invoice->purposeText(),
|
'purpose' => self::purpose($invoice),
|
||||||
'amount' => Amount::fromString($invoice->amount),
|
'amount' => Amount::fromString($invoice->amount),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -167,6 +176,25 @@ class CreateIncomeSurplusStatementCommand
|
|||||||
return ['groups' => $groups, 'total' => $total];
|
return ['groups' => $groups, 'total' => $total];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Der Zweck eines Belegs für die Anlage.
|
||||||
|
*
|
||||||
|
* Erfasster oder abgeleiteter Zahlungsgrund wie in der Beleg-Übersicht. Nur wenn es keinen gibt --
|
||||||
|
* ältere Abrechnungen ohne jede Zweckangabe --, steht dort ersatzweise, wem was erstattet wurde. Bewusst
|
||||||
|
* hier und nicht in {@see Invoice::purposeText()}: Dort würde der Ersatztext als erfasster Zahlungsgrund
|
||||||
|
* erscheinen und beim Korrigieren als Änderung protokolliert.
|
||||||
|
*/
|
||||||
|
private static function purpose(Invoice $invoice): string
|
||||||
|
{
|
||||||
|
$purpose = $invoice->purposeText();
|
||||||
|
|
||||||
|
if (trim($purpose) !== '') {
|
||||||
|
return $purpose;
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim('Auslagenerstattung ' . $invoice->contact_name);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ein Betrag in deutscher Schreibweise: Punkt als Tausender-, Komma als Dezimaltrennzeichen.
|
* Ein Betrag in deutscher Schreibweise: Punkt als Tausender-, Komma als Dezimaltrennzeichen.
|
||||||
*
|
*
|
||||||
|
|||||||
+7
@@ -29,5 +29,12 @@ class CreateIncomeSurplusStatementResponse
|
|||||||
*/
|
*/
|
||||||
public array $expenses = [];
|
public array $expenses = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die gespendeten Auslagen, gegliedert wie die Ausgaben -- die Gegenbuchung zur Zeile „Spenden".
|
||||||
|
*
|
||||||
|
* @var array{groups: array<int, array{name: string, sum: Amount, rows: array<int, array<string, mixed>>}>, total: Amount}|array{}
|
||||||
|
*/
|
||||||
|
public array $donations = [];
|
||||||
|
|
||||||
public ?Amount $result = null;
|
public ?Amount $result = null;
|
||||||
}
|
}
|
||||||
@@ -2,9 +2,11 @@
|
|||||||
|
|
||||||
namespace App\Domains\Event\Actions\SetPaymentMethods;
|
namespace App\Domains\Event\Actions\SetPaymentMethods;
|
||||||
|
|
||||||
|
use App\EventPaymentModules\EventPaymentModuleRegistry;
|
||||||
use App\Models\AvailablePaymentMethod;
|
use App\Models\AvailablePaymentMethod;
|
||||||
use App\Models\PaymentMethod;
|
use App\Models\PaymentMethod;
|
||||||
use App\RelationModels\EventPaymentMethods;
|
use App\RelationModels\EventPaymentMethods;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
class SetPaymentMethodsCommand
|
class SetPaymentMethodsCommand
|
||||||
{
|
{
|
||||||
@@ -25,7 +27,22 @@ class SetPaymentMethodsCommand
|
|||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->syncPaymentMethods();
|
$existing = EventPaymentMethods::where('event_id', $this->request->event->id)->get()->keyBy('slug');
|
||||||
|
$configurations = $this->configurationsToWrite($existing);
|
||||||
|
|
||||||
|
// Erst prüfen, dann schreiben: Eine Zahlungsart, die am Event unvollständig konfiguriert wäre,
|
||||||
|
// ließe sich bei der Anmeldung wählen, ohne dass die Angaben für die Zahlung vorliegen.
|
||||||
|
$incomplete = $this->incompleteNames($configurations);
|
||||||
|
if ($incomplete !== []) {
|
||||||
|
$response->success = false;
|
||||||
|
$response->message = sprintf(
|
||||||
|
'Die Konfiguration von „%s" ist unvollständig oder ungültig.',
|
||||||
|
implode('", „', $incomplete),
|
||||||
|
);
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->syncPaymentMethods($existing, $configurations);
|
||||||
|
|
||||||
$this->request->event->save();
|
$this->request->event->save();
|
||||||
$response->success = true;
|
$response->success = true;
|
||||||
@@ -34,48 +51,87 @@ class SetPaymentMethodsCommand
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Differenzieller Sync der Zahlungsmethoden mit Snapshot-Copy-Semantik:
|
* Die Configs, die geschrieben werden (Snapshot-Copy-Semantik):
|
||||||
* - entfernte Methoden werden gelöscht,
|
|
||||||
* - neue Methoden erhalten einen Snapshot der Tenant-Config (oder eines expliziten Overrides),
|
* - neue Methoden erhalten einen Snapshot der Tenant-Config (oder eines expliziten Overrides),
|
||||||
* - bestehende Methoden behalten ihre pro-Event-Config, sofern kein Override übergeben wird.
|
* - bestehende Methoden behalten ihre pro-Event-Config, sofern kein Override übergeben wird --
|
||||||
|
* sie tauchen hier dann nicht auf.
|
||||||
|
*
|
||||||
|
* @param Collection<string, EventPaymentMethods> $existing
|
||||||
|
* @return array<string, array<string, mixed>> slug => Config
|
||||||
*/
|
*/
|
||||||
private function syncPaymentMethods(): void
|
private function configurationsToWrite(Collection $existing): array
|
||||||
{
|
{
|
||||||
$event = $this->request->event;
|
|
||||||
$desiredSlugs = $this->request->paymentMethods;
|
$desiredSlugs = $this->request->paymentMethods;
|
||||||
$overrides = $this->request->paymentMethodConfigurations;
|
$overrides = $this->request->paymentMethodConfigurations;
|
||||||
|
|
||||||
$existing = EventPaymentMethods::where('event_id', $event->id)->get()->keyBy('slug');
|
|
||||||
|
|
||||||
// Nicht mehr gewünschte Methoden entfernen.
|
|
||||||
foreach ($existing as $slug => $row) {
|
|
||||||
if (!in_array($slug, $desiredSlugs, true)) {
|
|
||||||
$row->delete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tenant-Instanzen (für Snapshot-Kopie neuer Methoden) laden.
|
// Tenant-Instanzen (für Snapshot-Kopie neuer Methoden) laden.
|
||||||
$tenantMethods = AvailablePaymentMethod::whereIn('slug', $desiredSlugs)->get()->keyBy('slug');
|
$tenantMethods = AvailablePaymentMethod::whereIn('slug', $desiredSlugs)->get()->keyBy('slug');
|
||||||
|
|
||||||
|
$configurations = [];
|
||||||
foreach ($desiredSlugs as $slug) {
|
foreach ($desiredSlugs as $slug) {
|
||||||
$override = $overrides[$slug] ?? null;
|
$override = $overrides[$slug] ?? null;
|
||||||
|
|
||||||
if (isset($existing[$slug])) {
|
if (isset($existing[$slug]) && $override === null) {
|
||||||
// Bestehende Zuweisung: Config nur bei explizitem Override anpassen, sonst unverändert lassen.
|
|
||||||
if ($override !== null) {
|
|
||||||
$row = $existing[$slug];
|
|
||||||
$row->configuration = $this->eventConfiguration($slug, $override);
|
|
||||||
$row->save();
|
|
||||||
}
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Neue Zuweisung: expliziter Override oder Snapshot der Tenant-Config.
|
|
||||||
$snapshot = $override ?? ($tenantMethods[$slug]->configuration ?? []);
|
$snapshot = $override ?? ($tenantMethods[$slug]->configuration ?? []);
|
||||||
|
$configurations[$slug] = $this->eventConfiguration($slug, $snapshot ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $configurations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Namen der Zahlungsarten, deren Config nicht vollständig oder nicht gültig ist.
|
||||||
|
*
|
||||||
|
* Geprüft wird auf der Event-Config, also ohne die tenant-weiten Optionen -- die sind ohnehin nie
|
||||||
|
* Pflicht, weil sie am Event gar nicht liegen.
|
||||||
|
*
|
||||||
|
* @param array<string, array<string, mixed>> $configurations
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private function incompleteNames(array $configurations): array
|
||||||
|
{
|
||||||
|
$names = [];
|
||||||
|
foreach ($configurations as $slug => $configuration) {
|
||||||
|
if (!PaymentMethod::isConfigurationComplete($slug, $configuration)) {
|
||||||
|
$names[] = EventPaymentModuleRegistry::forSlug($slug)?->defaultName() ?? $slug;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $names;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Differenzieller Sync: entfernte Methoden werden gelöscht, die vorbereiteten Configs geschrieben.
|
||||||
|
*
|
||||||
|
* @param Collection<string, EventPaymentMethods> $existing
|
||||||
|
* @param array<string, array<string, mixed>> $configurations
|
||||||
|
*/
|
||||||
|
private function syncPaymentMethods(Collection $existing, array $configurations): void
|
||||||
|
{
|
||||||
|
$event = $this->request->event;
|
||||||
|
|
||||||
|
// Nicht mehr gewünschte Methoden entfernen.
|
||||||
|
foreach ($existing as $slug => $row) {
|
||||||
|
if (!in_array($slug, $this->request->paymentMethods, true)) {
|
||||||
|
$row->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($configurations as $slug => $configuration) {
|
||||||
|
if (isset($existing[$slug])) {
|
||||||
|
$row = $existing[$slug];
|
||||||
|
$row->configuration = $configuration;
|
||||||
|
$row->save();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
EventPaymentMethods::create([
|
EventPaymentMethods::create([
|
||||||
'event_id' => $event->id,
|
'event_id' => $event->id,
|
||||||
'slug' => $slug,
|
'slug' => $slug,
|
||||||
'configuration' => $this->eventConfiguration($slug, $snapshot ?? []),
|
'configuration' => $configuration,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ namespace App\Domains\Event\Actions\ShortSignUp;
|
|||||||
|
|
||||||
use App\Domains\Event\Actions\SignUp\SignUpCommand;
|
use App\Domains\Event\Actions\SignUp\SignUpCommand;
|
||||||
use App\Domains\Event\Actions\SignUp\SignUpRequest;
|
use App\Domains\Event\Actions\SignUp\SignUpRequest;
|
||||||
|
use App\Models\ParentGroup;
|
||||||
|
use App\Models\Tenant;
|
||||||
use App\ValueObjects\Age;
|
use App\ValueObjects\Age;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,12 +31,20 @@ class ShortSignUpCommand {
|
|||||||
|
|
||||||
// Ohne konfigurierte Teilnahmegruppe gäbe es keinen gültigen `participation_type` -- sauber abbrechen,
|
// Ohne konfigurierte Teilnahmegruppe gäbe es keinen gültigen `participation_type` -- sauber abbrechen,
|
||||||
// statt in einen Fremdschlüssel-Fehler zu laufen.
|
// statt in einen Fremdschlüssel-Fehler zu laufen.
|
||||||
$participationType = $this->request->event->firstParticipationTypeSlug();
|
$participationFee = $this->request->event->firstParticipationFee();
|
||||||
if ($participationType === null) {
|
if ($participationFee === null) {
|
||||||
$response->message = 'Für diese Veranstaltung ist noch keine Teilnahmegruppe hinterlegt.';
|
$response->message = 'Für diese Veranstaltung ist noch keine Teilnahmegruppe hinterlegt.';
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$participationType = $participationFee->type;
|
||||||
|
|
||||||
|
// Nur hinterlegte Beitragsstufen sind wählbar -- für jede andere hätte calculateAmount() keinen Betrag.
|
||||||
|
if (!in_array($this->request->feeType, $participationFee->availableFeeTypes(), true)) {
|
||||||
|
$response->message = 'Der gewählte Beitrag ist für diese Veranstaltung nicht verfügbar.';
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
$participantAge = new Age($this->request->birthday);
|
$participantAge = new Age($this->request->birthday);
|
||||||
|
|
||||||
// Bei Minderjährigen ist die Kontaktperson Pflicht; der Client prüft das ebenfalls, verlassen wird sich
|
// Bei Minderjährigen ist die Kontaktperson Pflicht; der Client prüft das ebenfalls, verlassen wird sich
|
||||||
@@ -45,28 +55,31 @@ class ShortSignUpCommand {
|
|||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stamm: bei genau einem teilnehmenden Stamm wird dieser gesetzt, bei mehreren muss gewählt werden,
|
// Gruppe (Local oder Parent Group, je nach Veranstaltung): bei genau einer Option wird diese gesetzt, bei
|
||||||
// ohne teilnehmende Stämme bleibt die Zuordnung leer.
|
// mehreren muss gewählt werden, ohne Optionen bleibt die Zuordnung leer.
|
||||||
$localGroups = $this->request->event->localGroups;
|
$groupOptions = $this->request->event->participantGroupOptions();
|
||||||
$localGroup = null;
|
$participantGroup = null;
|
||||||
|
|
||||||
if ($localGroups->count() === 1) {
|
if ($groupOptions->count() === 1) {
|
||||||
$localGroup = $localGroups->first();
|
$participantGroup = $groupOptions->first();
|
||||||
} elseif ($localGroups->count() > 1) {
|
} elseif ($groupOptions->count() > 1) {
|
||||||
$localGroup = $localGroups->firstWhere('id', $this->request->localGroupId);
|
$participantGroup = $groupOptions->firstWhere('id', $this->request->localGroupId);
|
||||||
|
|
||||||
if ($localGroup === null) {
|
if ($participantGroup === null) {
|
||||||
$response->message = 'Bitte einen Stamm auswählen.';
|
$response->message = sprintf(
|
||||||
|
'Bitte bei „%s" eine Auswahl treffen.',
|
||||||
|
$this->request->event->participantGroupType()->label(),
|
||||||
|
);
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$eventResource = $this->request->event->toResource();
|
$eventResource = $this->request->event->toResource();
|
||||||
|
|
||||||
// Zeitraum, Gruppe und Beitragsstufe stehen bei der Kurzanmeldung fest -- es gibt nichts zu wählen.
|
// Zeitraum und Gruppe stehen bei der Kurzanmeldung fest, wählbar ist nur die Beitragsstufe.
|
||||||
$amount = $eventResource->calculateAmount(
|
$amount = $eventResource->calculateAmount(
|
||||||
$participationType,
|
$participationType,
|
||||||
'standard',
|
$this->request->feeType,
|
||||||
$this->request->event->start_date,
|
$this->request->event->start_date,
|
||||||
$this->request->event->end_date,
|
$this->request->event->end_date,
|
||||||
false
|
false
|
||||||
@@ -79,7 +92,7 @@ class ShortSignUpCommand {
|
|||||||
lastname: $this->request->lastname,
|
lastname: $this->request->lastname,
|
||||||
nickname: $this->request->nickname,
|
nickname: $this->request->nickname,
|
||||||
participationType: $participationType,
|
participationType: $participationType,
|
||||||
localGroup: $localGroup,
|
localGroup: $participantGroup instanceof Tenant ? $participantGroup : null,
|
||||||
birthday: $this->request->birthday,
|
birthday: $this->request->birthday,
|
||||||
address_1: self::PLACEHOLDER,
|
address_1: self::PLACEHOLDER,
|
||||||
address_2: null,
|
address_2: null,
|
||||||
@@ -113,8 +126,9 @@ class ShortSignUpCommand {
|
|||||||
paymentOptions: $this->request->paymentOptions,
|
paymentOptions: $this->request->paymentOptions,
|
||||||
participationOptions: $this->request->participationOptions,
|
participationOptions: $this->request->participationOptions,
|
||||||
addonItems: [],
|
addonItems: [],
|
||||||
feeType: 'standard',
|
feeType: $this->request->feeType,
|
||||||
siblingReduction: false,
|
siblingReduction: false,
|
||||||
|
parentGroup: $participantGroup instanceof ParentGroup ? $participantGroup : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
$signUpResponse = new SignUpCommand($signUpRequest)->execute();
|
$signUpResponse = new SignUpCommand($signUpRequest)->execute();
|
||||||
|
|||||||
@@ -42,6 +42,11 @@ class ShortSignUpRequest {
|
|||||||
public array $paymentOptions = [],
|
public array $paymentOptions = [],
|
||||||
/** Antworten auf die Teilnahmeoptionen: [ question_key => value ]. */
|
/** Antworten auf die Teilnahmeoptionen: [ question_key => value ]. */
|
||||||
public array $participationOptions = [],
|
public array $participationOptions = [],
|
||||||
|
/**
|
||||||
|
* Beitragsstufe (`standard`, `reduced`, `solidarity`). Wählbar nur, wenn die Teilnahmegruppe neben dem
|
||||||
|
* Standardbeitrag weitere Stufen hinterlegt hat; der Command prüft das.
|
||||||
|
*/
|
||||||
|
public string $feeType = 'standard',
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ use App\Enumerations\EatingHabit;
|
|||||||
use App\Enumerations\EfzStatus;
|
use App\Enumerations\EfzStatus;
|
||||||
use App\Enumerations\SwimmingPermission;
|
use App\Enumerations\SwimmingPermission;
|
||||||
use App\EventPaymentModules\EventPaymentModuleRegistry;
|
use App\EventPaymentModules\EventPaymentModuleRegistry;
|
||||||
|
use App\Repositories\PaymentMethodRepository;
|
||||||
use App\ValueObjects\Age;
|
use App\ValueObjects\Age;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
@@ -17,6 +18,14 @@ class SignUpCommand {
|
|||||||
public function execute() : SignUpResponse {
|
public function execute() : SignUpResponse {
|
||||||
$response = new SignUpResponse();
|
$response = new SignUpResponse();
|
||||||
|
|
||||||
|
// Nur eine Zahlungsart, die am Event hängt, aktiv und vollständig konfiguriert ist.
|
||||||
|
if ($this->request->paymentMethod !== null
|
||||||
|
&& !new PaymentMethodRepository()->isUsableForSignUp($this->request->event, $this->request->paymentMethod)) {
|
||||||
|
$response->success = false;
|
||||||
|
$response->message = 'Die gewählte Zahlungsart steht für diese Veranstaltung nicht zur Verfügung.';
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
// Teilnehmer-Eingaben der gewählten Zahlungsart gegen das Modul-Schema absichern.
|
// Teilnehmer-Eingaben der gewählten Zahlungsart gegen das Modul-Schema absichern.
|
||||||
$module = $this->request->paymentMethod !== null
|
$module = $this->request->paymentMethod !== null
|
||||||
? EventPaymentModuleRegistry::forSlug($this->request->paymentMethod)
|
? EventPaymentModuleRegistry::forSlug($this->request->paymentMethod)
|
||||||
@@ -37,11 +46,7 @@ class SignUpCommand {
|
|||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
$eatingHabit = match ($this->request->eating_habit) {
|
$eatingHabit = $this->resolveEatingHabit();
|
||||||
'vegan' => EatingHabit::EATING_HABIT_VEGAN,
|
|
||||||
'vegetarian' => EatingHabit::EATING_HABIT_VEGETARIAN,
|
|
||||||
default => EatingHabit::EATING_HABIT_OMNIVOR,
|
|
||||||
};
|
|
||||||
|
|
||||||
$participantAge = new Age($this->request->birthday);
|
$participantAge = new Age($this->request->birthday);
|
||||||
|
|
||||||
@@ -61,6 +66,7 @@ class SignUpCommand {
|
|||||||
'fee_type' => $this->request->feeType,
|
'fee_type' => $this->request->feeType,
|
||||||
'sibling_reduction' => $this->request->siblingReduction,
|
'sibling_reduction' => $this->request->siblingReduction,
|
||||||
'local_group' => $this->request->localGroup?->slug,
|
'local_group' => $this->request->localGroup?->slug,
|
||||||
|
'parent_group_id' => $this->request->parentGroup?->id,
|
||||||
'birthday' => $this->request->birthday,
|
'birthday' => $this->request->birthday,
|
||||||
'address_1' => $this->request->address_1,
|
'address_1' => $this->request->address_1,
|
||||||
'address_2' => $this->request->address_2,
|
'address_2' => $this->request->address_2,
|
||||||
@@ -94,7 +100,9 @@ class SignUpCommand {
|
|||||||
'payment_purpose' => $this->request->event->name . ' - Beitrag ' . $this->request->firstname . ' ' . $this->request->lastname,
|
'payment_purpose' => $this->request->event->name . ' - Beitrag ' . $this->request->firstname . ' ' . $this->request->lastname,
|
||||||
'payment_method' => $this->request->paymentMethod,
|
'payment_method' => $this->request->paymentMethod,
|
||||||
'payment_options' => $paymentOptions,
|
'payment_options' => $paymentOptions,
|
||||||
'efz_status' => $participantAge->isfullAged() ? EfzStatus::EFZ_STATUS_NOT_CHECKED : EfzStatus::EFZ_STATUS_NOT_REQUIRED,
|
'efz_status' => $participantAge->isfullAged() && $this->request->event->coc_check_required
|
||||||
|
? EfzStatus::EFZ_STATUS_NOT_CHECKED
|
||||||
|
: EfzStatus::EFZ_STATUS_NOT_REQUIRED,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -108,6 +116,21 @@ class SignUpCommand {
|
|||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ernährungsweise auflösen. Übernommen wird nur ein Slug, den die Veranstaltung anbietet; ohne Angabe (z. B. bei
|
||||||
|
* der Kurzanmeldung) oder bei einem nicht angebotenen Wert gilt "Omnivor".
|
||||||
|
*/
|
||||||
|
private function resolveEatingHabit(): string
|
||||||
|
{
|
||||||
|
$slug = $this->request->eating_habit;
|
||||||
|
|
||||||
|
if ($slug !== '' && $this->request->event->eatingHabits()->where('slug', $slug)->exists()) {
|
||||||
|
return $slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
return EatingHabit::EATING_HABIT_OMNIVOR;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Badeerlaubnis auflösen. Volljährige brauchen keine Erlaubnis. Für Minderjährige gilt die getroffene Auswahl;
|
* Badeerlaubnis auflösen. Volljährige brauchen keine Erlaubnis. Für Minderjährige gilt die getroffene Auswahl;
|
||||||
* fehlt sie -- weil die Veranstaltung die Badeerlaubnis gar nicht abfragt oder keine Wahl getroffen wurde --,
|
* fehlt sie -- weil die Veranstaltung die Badeerlaubnis gar nicht abfragt oder keine Wahl getroffen wurde --,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Domains\Event\Actions\SignUp;
|
namespace App\Domains\Event\Actions\SignUp;
|
||||||
|
|
||||||
use App\Models\Event;
|
use App\Models\Event;
|
||||||
|
use App\Models\ParentGroup;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\ValueObjects\Amount;
|
use App\ValueObjects\Amount;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
@@ -55,6 +56,8 @@ class SignUpRequest {
|
|||||||
public ?string $feeType = null,
|
public ?string $feeType = null,
|
||||||
/** Ob die 50-%-Geschwisterermäßigung in `amount` eingerechnet ist. */
|
/** Ob die 50-%-Geschwisterermäßigung in `amount` eingerechnet ist. */
|
||||||
public bool $siblingReduction = false,
|
public bool $siblingReduction = false,
|
||||||
|
/** Parent Group -- nur bei Veranstaltungen, die danach statt nach der Local Group fragen. */
|
||||||
|
public ?ParentGroup $parentGroup = null,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Domains\Event\Actions\UpdateEvent;
|
namespace App\Domains\Event\Actions\UpdateEvent;
|
||||||
|
|
||||||
|
use App\Repositories\EventParticipantRepository;
|
||||||
|
|
||||||
class UpdateEventCommand {
|
class UpdateEventCommand {
|
||||||
public UpdateEventRequest $request;
|
public UpdateEventRequest $request;
|
||||||
public function __construct(UpdateEventRequest $request) {
|
public function __construct(UpdateEventRequest $request) {
|
||||||
@@ -26,6 +28,15 @@ class UpdateEventCommand {
|
|||||||
$this->request->event->registration_allowed = $this->request->registrationAllowed;
|
$this->request->event->registration_allowed = $this->request->registrationAllowed;
|
||||||
$this->request->event->short_registration = $this->request->shortRegistration;
|
$this->request->event->short_registration = $this->request->shortRegistration;
|
||||||
$this->request->event->swimming_permission_required = $this->request->swimmingPermissionRequired;
|
$this->request->event->swimming_permission_required = $this->request->swimmingPermissionRequired;
|
||||||
|
$this->request->event->coc_check_required = $this->request->cocCheckRequired;
|
||||||
|
|
||||||
|
// Umstellen zwischen Local und Parent Group nur ohne Anmeldungen -- sonst stünden in einer Veranstaltung
|
||||||
|
// beide Arten von Antworten nebeneinander.
|
||||||
|
if ($this->request->participantGroupType !== null
|
||||||
|
&& !new EventParticipantRepository()->existsForEvent($this->request->event)) {
|
||||||
|
$this->request->event->participant_group_type = $this->request->participantGroupType->value;
|
||||||
|
}
|
||||||
|
|
||||||
$this->request->event->save();
|
$this->request->event->save();
|
||||||
|
|
||||||
$this->request->event->resetAllowedEatingHabits();
|
$this->request->event->resetAllowedEatingHabits();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Domains\Event\Actions\UpdateEvent;
|
namespace App\Domains\Event\Actions\UpdateEvent;
|
||||||
|
|
||||||
|
use App\Enumerations\ParticipantGroupType;
|
||||||
use App\Models\Event;
|
use App\Models\Event;
|
||||||
use App\ValueObjects\Amount;
|
use App\ValueObjects\Amount;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
@@ -21,13 +22,18 @@ class UpdateEventRequest {
|
|||||||
public bool $registrationAllowed;
|
public bool $registrationAllowed;
|
||||||
public bool $shortRegistration;
|
public bool $shortRegistration;
|
||||||
public bool $swimmingPermissionRequired;
|
public bool $swimmingPermissionRequired;
|
||||||
|
public bool $cocCheckRequired;
|
||||||
public Amount $flatSupport;
|
public Amount $flatSupport;
|
||||||
public Amount $supportPerPerson;
|
public Amount $supportPerPerson;
|
||||||
public array $contributingLocalGroups;
|
public array $contributingLocalGroups;
|
||||||
public array $eatingHabits;
|
public array $eatingHabits;
|
||||||
|
/** Wonach bei der Anmeldung gefragt wird; null = unverändert. Greift nur, solange es keine Anmeldungen gibt. */
|
||||||
|
public ?ParticipantGroupType $participantGroupType;
|
||||||
|
|
||||||
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, bool $shortRegistration = false, bool $swimmingPermissionRequired = true, ?string $street = null, ?string $houseNumber = null)
|
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, bool $shortRegistration = false, bool $swimmingPermissionRequired = true, ?string $street = null, ?string $houseNumber = null, ?ParticipantGroupType $participantGroupType = null, bool $cocCheckRequired = true)
|
||||||
{
|
{
|
||||||
|
$this->participantGroupType = $participantGroupType;
|
||||||
|
$this->cocCheckRequired = $cocCheckRequired;
|
||||||
$this->event = $event;
|
$this->event = $event;
|
||||||
$this->eventName = $eventName;
|
$this->eventName = $eventName;
|
||||||
$this->eventLocation = $eventLocation;
|
$this->eventLocation = $eventLocation;
|
||||||
|
|||||||
@@ -31,8 +31,7 @@ class UpdateParticipantCommand {
|
|||||||
$p->address_2 = $this->request->address_2;
|
$p->address_2 = $this->request->address_2;
|
||||||
$p->postcode = $this->request->postcode;
|
$p->postcode = $this->request->postcode;
|
||||||
$p->city = $this->request->city;
|
$p->city = $this->request->city;
|
||||||
// Leerer String würde als Fremdschlüssel scheitern -- kein Stamm heißt null.
|
$this->applyParticipantGroup($p);
|
||||||
$p->local_group = $this->request->localgroup ?: null;
|
|
||||||
$p->birthday = DateTime::createFromFormat('Y-m-d', $this->request->birthday);
|
$p->birthday = DateTime::createFromFormat('Y-m-d', $this->request->birthday);
|
||||||
$p->email_1 = $this->request->email_1;
|
$p->email_1 = $this->request->email_1;
|
||||||
$p->phone_1 = $this->request->phone_1;
|
$p->phone_1 = $this->request->phone_1;
|
||||||
@@ -87,6 +86,29 @@ class UpdateParticipantCommand {
|
|||||||
return $this->response;
|
return $this->response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Übernimmt die gewählte Gruppe -- je nach Veranstaltung die Local Group (Slug) oder die Parent Group (ID).
|
||||||
|
* Angenommen wird nur, was die Veranstaltung zur Auswahl stellt; leer heißt keine Angabe (null, ein leerer
|
||||||
|
* String würde als Fremdschlüssel scheitern).
|
||||||
|
*/
|
||||||
|
private function applyParticipantGroup(EventParticipant $participant): void
|
||||||
|
{
|
||||||
|
$event = $participant->event;
|
||||||
|
$value = trim((string) $this->request->participantGroup);
|
||||||
|
|
||||||
|
if ($event->asksForParentGroup()) {
|
||||||
|
$participant->parent_group_id = $value === ''
|
||||||
|
? null
|
||||||
|
: $event->participantGroupOptions()->firstWhere('id', (int) $value)?->id;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$participant->local_group = $value === ''
|
||||||
|
? null
|
||||||
|
: $event->participantGroupOptions()->firstWhere('slug', $value)?->slug;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ersetzt die zugeordneten Zusätze (Event-Addons) anhand der Auswahl. Bewusst **ohne Preis/Betrag**
|
* Ersetzt die zugeordneten Zusätze (Event-Addons) anhand der Auswahl. Bewusst **ohne Preis/Betrag**
|
||||||
* (amount 0): der Teilnahmebeitrag wird in den Details ohnehin manuell gesetzt. Es wird nur der Titel als
|
* (amount 0): der Teilnahmebeitrag wird in den Details ohnehin manuell gesetzt. Es wird nur der Titel als
|
||||||
|
|||||||
@@ -14,8 +14,11 @@ class UpdateParticipantRequest {
|
|||||||
public ?string $address_2,
|
public ?string $address_2,
|
||||||
public string $postcode,
|
public string $postcode,
|
||||||
public string $city,
|
public string $city,
|
||||||
/** Stamm-Slug; leer/null bei Anmeldungen, die keinen Stamm erfasst haben (Kurzanmeldung). */
|
/**
|
||||||
public ?string $localgroup,
|
* Gewählte Gruppe: Slug der Local Group bzw. ID der Parent Group, je nachdem, wonach die Veranstaltung
|
||||||
|
* fragt. Leer/null = keine Angabe.
|
||||||
|
*/
|
||||||
|
public ?string $participantGroup,
|
||||||
public string $birthday,
|
public string $birthday,
|
||||||
public string $email_1,
|
public string $email_1,
|
||||||
public string $phone_1,
|
public string $phone_1,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use App\Domains\Event\Actions\CreateEvent\CreateEventRequest;
|
|||||||
use App\Domains\Event\Actions\SetCostUnit\SetCostUnitCommand;
|
use App\Domains\Event\Actions\SetCostUnit\SetCostUnitCommand;
|
||||||
use App\Domains\Event\Actions\SetCostUnit\SetCostUnitRequest;
|
use App\Domains\Event\Actions\SetCostUnit\SetCostUnitRequest;
|
||||||
use App\Enumerations\CostUnitType;
|
use App\Enumerations\CostUnitType;
|
||||||
|
use App\Enumerations\ParticipantGroupType;
|
||||||
use App\Enumerations\ParticipationFeeType;
|
use App\Enumerations\ParticipationFeeType;
|
||||||
use App\Providers\InertiaProvider;
|
use App\Providers\InertiaProvider;
|
||||||
use App\Resources\EventResource;
|
use App\Resources\EventResource;
|
||||||
@@ -28,6 +29,7 @@ class CreateController extends CommonController {
|
|||||||
'participationFeeType' => $this->tenant->slug === 'lv' ?
|
'participationFeeType' => $this->tenant->slug === 'lv' ?
|
||||||
ParticipationFeeType::PARTICIPATION_FEE_TYPE_FIXED :
|
ParticipationFeeType::PARTICIPATION_FEE_TYPE_FIXED :
|
||||||
ParticipationFeeType::PARTICIPATION_FEE_TYPE_SOLIDARITY,
|
ParticipationFeeType::PARTICIPATION_FEE_TYPE_SOLIDARITY,
|
||||||
|
'participantGroupTypes' => ParticipantGroupType::options(),
|
||||||
])->render();
|
])->render();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +60,8 @@ class CreateController extends CommonController {
|
|||||||
$request->input('eventIban'),
|
$request->input('eventIban'),
|
||||||
$payPerDay,
|
$payPerDay,
|
||||||
$request->input('eventStreet'),
|
$request->input('eventStreet'),
|
||||||
$request->input('eventHouseNumber')
|
$request->input('eventHouseNumber'),
|
||||||
|
ParticipantGroupType::fromStored($request->input('eventParticipantGroupType')),
|
||||||
);
|
);
|
||||||
|
|
||||||
$wasSuccessful = false;
|
$wasSuccessful = false;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use App\Domains\Event\Actions\UpdateEvent\UpdateEventCommand;
|
|||||||
use App\Domains\Event\Actions\UpdateEvent\UpdateEventRequest;
|
use App\Domains\Event\Actions\UpdateEvent\UpdateEventRequest;
|
||||||
use App\Domains\Event\Actions\UpdateManagers\UpdateManagersCommand;
|
use App\Domains\Event\Actions\UpdateManagers\UpdateManagersCommand;
|
||||||
use App\Domains\Event\Actions\UpdateManagers\UpdateManagersRequest;
|
use App\Domains\Event\Actions\UpdateManagers\UpdateManagersRequest;
|
||||||
|
use App\Enumerations\ParticipantGroupType;
|
||||||
use App\Enumerations\ParticipationFeeType;
|
use App\Enumerations\ParticipationFeeType;
|
||||||
use App\Enumerations\ParticipationType;
|
use App\Enumerations\ParticipationType;
|
||||||
use App\Providers\InertiaProvider;
|
use App\Providers\InertiaProvider;
|
||||||
@@ -27,7 +28,10 @@ use Illuminate\Http\Response;
|
|||||||
class DetailsController extends CommonController {
|
class DetailsController extends CommonController {
|
||||||
public function __invoke(string $eventId) {
|
public function __invoke(string $eventId) {
|
||||||
$event = $this->events->getByIdentifier($eventId);
|
$event = $this->events->getByIdentifier($eventId);
|
||||||
return new InertiaProvider('Event/Details', ['event' => $event])->render();
|
return new InertiaProvider('Event/Details', [
|
||||||
|
'event' => $event,
|
||||||
|
'participantGroupLabel' => $event->participantGroupType()->label(),
|
||||||
|
])->render();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function summary(int $eventId, Request $request) : JsonResponse {
|
public function summary(int $eventId, Request $request) : JsonResponse {
|
||||||
@@ -65,6 +69,8 @@ class DetailsController extends CommonController {
|
|||||||
(bool)$request->input('swimmingPermissionRequired', true),
|
(bool)$request->input('swimmingPermissionRequired', true),
|
||||||
$request->input('street'),
|
$request->input('street'),
|
||||||
$request->input('houseNumber'),
|
$request->input('houseNumber'),
|
||||||
|
ParticipantGroupType::tryFrom((string) $request->input('participantGroupType')),
|
||||||
|
(bool)$request->input('cocCheckRequired', true),
|
||||||
);
|
);
|
||||||
|
|
||||||
$eventUpdateCommand = new UpdateEventCommand($eventUpdateRequest);
|
$eventUpdateCommand = new UpdateEventCommand($eventUpdateRequest);
|
||||||
@@ -185,6 +191,7 @@ class DetailsController extends CommonController {
|
|||||||
'rows' => $participants,
|
'rows' => $participants,
|
||||||
'kitchenRequirements' => $kitchenOverview,
|
'kitchenRequirements' => $kitchenOverview,
|
||||||
'participantsForKitchenList' => $this->eventParticipants->getParticipantsWithIntolerances($event, $request),
|
'participantsForKitchenList' => $this->eventParticipants->getParticipantsWithIntolerances($event, $request),
|
||||||
|
'participantGroupLabel' => $event->participantGroupType()->label(),
|
||||||
])->render();
|
])->render();
|
||||||
|
|
||||||
$pdf = PdfGenerateAndDownloadProvider::fromHtml(
|
$pdf = PdfGenerateAndDownloadProvider::fromHtml(
|
||||||
@@ -208,6 +215,9 @@ class DetailsController extends CommonController {
|
|||||||
$csv = view('csvs.' . $listType, [
|
$csv = view('csvs.' . $listType, [
|
||||||
'event' => $event->name,
|
'event' => $event->name,
|
||||||
'rows' => $participants,
|
'rows' => $participants,
|
||||||
|
// Die Anschriftsspalten beschreiben immer die Local Group, die Gruppenspalte die gewählte Gruppe.
|
||||||
|
'localGroupLabel' => ParticipantGroupType::LocalGroup->label(),
|
||||||
|
'participantGroupLabel' => $event->participantGroupType()->label(),
|
||||||
])->render();
|
])->render();
|
||||||
|
|
||||||
return response($csv, 200, [
|
return response($csv, 200, [
|
||||||
@@ -220,7 +230,7 @@ class DetailsController extends CommonController {
|
|||||||
$event = $this->events->getByIdentifier($eventId);
|
$event = $this->events->getByIdentifier($eventId);
|
||||||
switch ($listType) {
|
switch ($listType) {
|
||||||
case 'by-local-group':
|
case 'by-local-group':
|
||||||
$participants = $this->eventParticipants->groupByLocalGroup($event, $request);
|
$participants = $this->eventParticipants->groupByParticipantGroup($event, $request);
|
||||||
break;
|
break;
|
||||||
case 'by-participation-group':
|
case 'by-participation-group':
|
||||||
$participants = $this->eventParticipants->groupByParticipationType($event, $request);
|
$participants = $this->eventParticipants->groupByParticipationType($event, $request);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class ByGroupController extends CommonController
|
|||||||
$recipients = [];
|
$recipients = [];
|
||||||
switch ($groupType) {
|
switch ($groupType) {
|
||||||
case 'by-local-group':
|
case 'by-local-group':
|
||||||
$participants = $this->eventParticipants->groupByLocalGroup($event, $request, $request->input('groupName'));
|
$participants = $this->eventParticipants->groupByParticipantGroup($event, $request, $request->input('groupName'));
|
||||||
$recipients = $this->eventParticipants->getMailAddresses($participants[$request->input('groupName')]);
|
$recipients = $this->eventParticipants->getMailAddresses($participants[$request->input('groupName')]);
|
||||||
break;
|
break;
|
||||||
case 'by-participation-group':
|
case 'by-participation-group':
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class ParticipantUpdateController extends CommonController {
|
|||||||
address_2: $request->input('address_2'),
|
address_2: $request->input('address_2'),
|
||||||
postcode: $request->input('postcode'),
|
postcode: $request->input('postcode'),
|
||||||
city: $request->input('city'),
|
city: $request->input('city'),
|
||||||
localgroup: $request->input('localgroup'),
|
participantGroup: $request->input('participantGroup'),
|
||||||
birthday: $request->input('birthday'),
|
birthday: $request->input('birthday'),
|
||||||
email_1: $request->input('email_1'),
|
email_1: $request->input('email_1'),
|
||||||
phone_1: $request->input('phone_1'),
|
phone_1: $request->input('phone_1'),
|
||||||
|
|||||||
@@ -2,35 +2,45 @@
|
|||||||
|
|
||||||
namespace App\Domains\Event\Controllers;
|
namespace App\Domains\Event\Controllers;
|
||||||
|
|
||||||
|
use App\RelationModels\EventParticipationFee;
|
||||||
use App\Scopes\CommonController;
|
use App\Scopes\CommonController;
|
||||||
use App\ValueObjects\Amount;
|
use App\ValueObjects\Amount;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Beitrag einer Kurzanmeldung. Teilnahmegruppe, Beitragsstufe und Zeitraum stehen fest, deshalb braucht der
|
* Beiträge einer Kurzanmeldung. Teilnahmegruppe und Zeitraum stehen fest; geliefert wird der Betrag je
|
||||||
* Endpunkt keine Eingaben -- und der Betrag lässt sich vom Client nicht beeinflussen.
|
* hinterlegter Beitragsstufe, damit der Client zwischen ihnen wählen kann, ohne erneut zu fragen. Der Endpunkt
|
||||||
|
* braucht keine Eingaben -- berechnet wird der gespeicherte Betrag bei der Anmeldung ohnehin neu.
|
||||||
*
|
*
|
||||||
* `amountValue` steuert im Frontend, ob der Schritt "Zahlungsart" gezeigt wird.
|
* `amountValue` der gewählten Stufe steuert im Frontend, ob der Schritt "Zahlungsart" gezeigt wird; mehr als
|
||||||
|
* eine Stufe blendet den Schritt "Beitrag" ein.
|
||||||
*/
|
*/
|
||||||
class ShortCalculateAmountController extends CommonController {
|
class ShortCalculateAmountController extends CommonController {
|
||||||
public function __invoke(int $eventId) : JsonResponse {
|
public function __invoke(int $eventId) : JsonResponse {
|
||||||
$event = $this->events->getById($eventId, false);
|
$event = $this->events->getById($eventId, false);
|
||||||
|
|
||||||
$participationType = $event->firstParticipationTypeSlug();
|
$participationFee = $event->firstParticipationFee();
|
||||||
|
$eventResource = $event->toResource();
|
||||||
|
|
||||||
$amount = $participationType === null
|
$feeTypes = array_map(function (string $feeType) use ($event, $eventResource, $participationFee) {
|
||||||
|
$amount = $participationFee === null
|
||||||
? new Amount(0, 'Euro')
|
? new Amount(0, 'Euro')
|
||||||
: $event->toResource()->calculateAmount(
|
: $eventResource->calculateAmount(
|
||||||
$participationType,
|
$participationFee->type,
|
||||||
'standard',
|
$feeType,
|
||||||
$event->start_date,
|
$event->start_date,
|
||||||
$event->end_date,
|
$event->end_date,
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
return response()->json([
|
return [
|
||||||
|
'value' => $feeType,
|
||||||
|
'label' => EventParticipationFee::FEE_TYPE_LABELS[$feeType],
|
||||||
'amount' => $amount->toString(),
|
'amount' => $amount->toString(),
|
||||||
'amountValue' => $amount->getAmount(),
|
'amountValue' => $amount->getAmount(),
|
||||||
]);
|
];
|
||||||
|
}, $participationFee?->availableFeeTypes() ?? ['standard']);
|
||||||
|
|
||||||
|
return response()->json(['feeTypes' => $feeTypes]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ use App\Enumerations\FirstAidPermission;
|
|||||||
use App\Enumerations\SwimmingPermission;
|
use App\Enumerations\SwimmingPermission;
|
||||||
use App\Mail\ParticipantParticipationMails\EventSignUpSuccessfullMail;
|
use App\Mail\ParticipantParticipationMails\EventSignUpSuccessfullMail;
|
||||||
use App\Providers\DoubleCheckEventRegistrationProvider;
|
use App\Providers\DoubleCheckEventRegistrationProvider;
|
||||||
|
use App\RelationModels\EventParticipationFee;
|
||||||
use App\Scopes\CommonController;
|
use App\Scopes\CommonController;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
@@ -53,6 +54,8 @@ class ShortSignupController extends CommonController {
|
|||||||
'participationOptions' => 'array',
|
'participationOptions' => 'array',
|
||||||
'paymentMethod' => 'nullable|string',
|
'paymentMethod' => 'nullable|string',
|
||||||
'paymentOptions' => 'array',
|
'paymentOptions' => 'array',
|
||||||
|
// Ob die Stufe für die Teilnahmegruppe hinterlegt ist, prüft der Command.
|
||||||
|
'feeType' => 'nullable|string|in:' . implode(',', array_keys(EventParticipationFee::FEE_TYPE_LABELS)),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($validator->fails()) {
|
if ($validator->fails()) {
|
||||||
@@ -99,6 +102,7 @@ class ShortSignupController extends CommonController {
|
|||||||
paymentMethod: $request->input('paymentMethod') ?: null,
|
paymentMethod: $request->input('paymentMethod') ?: null,
|
||||||
paymentOptions: (array)$request->input('paymentOptions', []),
|
paymentOptions: (array)$request->input('paymentOptions', []),
|
||||||
participationOptions: (array)$request->input('participationOptions', []),
|
participationOptions: (array)$request->input('participationOptions', []),
|
||||||
|
feeType: $request->input('feeType') ?: 'standard',
|
||||||
);
|
);
|
||||||
|
|
||||||
$shortSignUpResponse = new ShortSignUpCommand($shortSignUpRequest)->execute();
|
$shortSignUpResponse = new ShortSignUpCommand($shortSignUpRequest)->execute();
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use App\Domains\Event\Actions\CertificateOfConductionCheck\CertificateOfConducti
|
|||||||
use App\Domains\Event\Actions\SignUp\SignUpCommand;
|
use App\Domains\Event\Actions\SignUp\SignUpCommand;
|
||||||
use App\Domains\Event\Actions\SignUp\SignUpRequest;
|
use App\Domains\Event\Actions\SignUp\SignUpRequest;
|
||||||
use App\Mail\ParticipantParticipationMails\EventSignUpSuccessfullMail;
|
use App\Mail\ParticipantParticipationMails\EventSignUpSuccessfullMail;
|
||||||
|
use App\Models\ParentGroup;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Providers\DoubleCheckEventRegistrationProvider;
|
use App\Providers\DoubleCheckEventRegistrationProvider;
|
||||||
use App\Providers\InertiaProvider;
|
use App\Providers\InertiaProvider;
|
||||||
@@ -65,7 +66,7 @@ class SignupController extends CommonController {
|
|||||||
$inertiaProvider = new InertiaProvider('Event/Signup', [
|
$inertiaProvider = new InertiaProvider('Event/Signup', [
|
||||||
'event' => $event,
|
'event' => $event,
|
||||||
'availableEvents' => $availableEvents,
|
'availableEvents' => $availableEvents,
|
||||||
'localGroups' => $event['contributingLocalGroups'],
|
'localGroups' => $event['participantGroupOptions'] ?? [],
|
||||||
'participantData' => $participantData,
|
'participantData' => $participantData,
|
||||||
]);
|
]);
|
||||||
return $inertiaProvider->render();
|
return $inertiaProvider->render();
|
||||||
@@ -111,6 +112,15 @@ class SignupController extends CommonController {
|
|||||||
$addonResolution = $eventResource->resolveAddons($selectedAddonKeys, $arrival, $departure);
|
$addonResolution = $eventResource->resolveAddons($selectedAddonKeys, $arrival, $departure);
|
||||||
$amount->addAmount($addonResolution['total']);
|
$amount->addAmount($addonResolution['total']);
|
||||||
|
|
||||||
|
// Gewählt wird eine der Optionen der Veranstaltung (Local oder Parent Group); anderes wird abgelehnt.
|
||||||
|
$participantGroup = $event->participantGroupOptions()->firstWhere('id', (int)($registrationData['localGroup'] ?? 0));
|
||||||
|
if ($participantGroup === null) {
|
||||||
|
return response()->json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => sprintf('Bitte bei „%s" eine Auswahl treffen.', $event->participantGroupType()->label()),
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
$signupRequest = new SignUpRequest(
|
$signupRequest = new SignUpRequest(
|
||||||
$event,
|
$event,
|
||||||
$registrationData['userId'] ?? null,
|
$registrationData['userId'] ?? null,
|
||||||
@@ -118,7 +128,7 @@ class SignupController extends CommonController {
|
|||||||
$registrationData['nachname'],
|
$registrationData['nachname'],
|
||||||
$registrationData['pfadiname'],
|
$registrationData['pfadiname'],
|
||||||
$registrationData['participationType'],
|
$registrationData['participationType'],
|
||||||
Tenant::findOrFail($registrationData['localGroup']),
|
$participantGroup instanceof Tenant ? $participantGroup : null,
|
||||||
\DateTime::createFromFormat('Y-m-d', $registrationData['geburtsdatum']),
|
\DateTime::createFromFormat('Y-m-d', $registrationData['geburtsdatum']),
|
||||||
$registrationData['address1'],
|
$registrationData['address1'],
|
||||||
$registrationData['address2'],
|
$registrationData['address2'],
|
||||||
@@ -133,7 +143,7 @@ class SignupController extends CommonController {
|
|||||||
$registrationData['intolerances'],
|
$registrationData['intolerances'],
|
||||||
$registrationData['medikamente'],
|
$registrationData['medikamente'],
|
||||||
$tetanusVaccination,
|
$tetanusVaccination,
|
||||||
$registrationData['essgewohnheit'],
|
$registrationData['eatingHabit'] ?? '',
|
||||||
$registrationData['badeerlaubnis'],
|
$registrationData['badeerlaubnis'],
|
||||||
$registrationData['first_aid'],
|
$registrationData['first_aid'],
|
||||||
$registrationData['foto']['socialmedia'],
|
$registrationData['foto']['socialmedia'],
|
||||||
@@ -153,6 +163,7 @@ class SignupController extends CommonController {
|
|||||||
$addonResolution['items'],
|
$addonResolution['items'],
|
||||||
$registrationData['beitrag'],
|
$registrationData['beitrag'],
|
||||||
$siblingReduction,
|
$siblingReduction,
|
||||||
|
$participantGroup instanceof ParentGroup ? $participantGroup : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
$signupCommand = new SignUpCommand($signupRequest);
|
$signupCommand = new SignUpCommand($signupRequest);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
'eventIban': String,
|
'eventIban': String,
|
||||||
"eventPayPerDay": Boolean,
|
"eventPayPerDay": Boolean,
|
||||||
"participationFeeType": String,
|
"participationFeeType": String,
|
||||||
|
"participantGroupTypes": {type: Array, default: () => []},
|
||||||
})
|
})
|
||||||
|
|
||||||
const errors = reactive({})
|
const errors = reactive({})
|
||||||
@@ -35,6 +36,7 @@
|
|||||||
eventIban: props.eventIban ? props.eventIban : '',
|
eventIban: props.eventIban ? props.eventIban : '',
|
||||||
eventPayPerDay: props.eventPayPerDay ? props.eventPayPerDay : false,
|
eventPayPerDay: props.eventPayPerDay ? props.eventPayPerDay : false,
|
||||||
eventParticipationFeeType: props.participationFeeType ? props.participationFeeType : 'fixed',
|
eventParticipationFeeType: props.participationFeeType ? props.participationFeeType : 'fixed',
|
||||||
|
eventParticipantGroupType: 'local_group',
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -159,6 +161,7 @@
|
|||||||
eventIban: formData.eventIban,
|
eventIban: formData.eventIban,
|
||||||
eventPayPerDay: formData.eventPayPerDay,
|
eventPayPerDay: formData.eventPayPerDay,
|
||||||
eventParticipationFeeType: formData.eventParticipationFeeType,
|
eventParticipationFeeType: formData.eventParticipationFeeType,
|
||||||
|
eventParticipantGroupType: formData.eventParticipantGroupType,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -256,6 +259,15 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
<tr style="vertical-align: top;">
|
||||||
|
<th class="width-medium pr-20 height-50">Bei der Anmeldung abfragen</th>
|
||||||
|
<td class="height-50">
|
||||||
|
<select v-model="formData.eventParticipantGroupType" class="width-half-full">
|
||||||
|
<option v-for="type in props.participantGroupTypes" :key="type.value" :value="type.value">{{ type.label }}</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
<tr style="vertical-align: top;">
|
<tr style="vertical-align: top;">
|
||||||
<th class="width-medium pr-20 height-50">Preiserhöhung nach Early-Bird-Phase</th>
|
<th class="width-medium pr-20 height-50">Preiserhöhung nach Early-Bird-Phase</th>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import Overview from "./Partials/Overview.vue";
|
|||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
event: Object,
|
event: Object,
|
||||||
|
participantGroupLabel: {type: String, default: 'Stamm'},
|
||||||
})
|
})
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
@@ -24,7 +25,7 @@ const tabs = [
|
|||||||
endpoint: "/api/v1/event/details/" + props.event.identifier + '/participants/all',
|
endpoint: "/api/v1/event/details/" + props.event.identifier + '/participants/all',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Teilis nach Stamm',
|
title: 'Teilis nach ' + props.participantGroupLabel,
|
||||||
component: ParticipantsList,
|
component: ParticipantsList,
|
||||||
endpoint: "/api/v1/event/details/" + props.event.identifier + '/participants/by-local-group',
|
endpoint: "/api/v1/event/details/" + props.event.identifier + '/participants/by-local-group',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,8 +16,13 @@ const emit = defineEmits(['close'])
|
|||||||
const dynmicProps = reactive({
|
const dynmicProps = reactive({
|
||||||
localGroups: [],
|
localGroups: [],
|
||||||
eatingHabits:[],
|
eatingHabits:[],
|
||||||
|
participantGroupTypes: [],
|
||||||
|
localGroupLabelPlural: 'Stämme',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Umstellen zwischen Local und Parent Group nur, solange es keine Anmeldungen gibt.
|
||||||
|
const participantGroupTypeLocked = (props.event.totalParticipantCount ?? 0) > 0
|
||||||
|
|
||||||
const contributingLocalGroups = ref([])
|
const contributingLocalGroups = ref([])
|
||||||
const eatingHabits = ref([]);
|
const eatingHabits = ref([]);
|
||||||
|
|
||||||
@@ -39,8 +44,10 @@ const emit = defineEmits(['close'])
|
|||||||
registrationAllowed: props.event.registrationAllowed,
|
registrationAllowed: props.event.registrationAllowed,
|
||||||
shortRegistration: props.event.shortRegistration,
|
shortRegistration: props.event.shortRegistration,
|
||||||
swimmingPermissionRequired: props.event.swimmingPermissionRequired,
|
swimmingPermissionRequired: props.event.swimmingPermissionRequired,
|
||||||
|
cocCheckRequired: props.event.cocCheckRequired,
|
||||||
flatSupport: props.event.flatSupportEdit,
|
flatSupport: props.event.flatSupportEdit,
|
||||||
supportPerson: props.event.supportPersonIndex,
|
supportPerson: props.event.supportPersonIndex,
|
||||||
|
participantGroupType: props.event.participantGroupType ?? 'local_group',
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -72,10 +79,12 @@ const emit = defineEmits(['close'])
|
|||||||
registrationAllowed: formData.registrationAllowed,
|
registrationAllowed: formData.registrationAllowed,
|
||||||
shortRegistration: formData.shortRegistration,
|
shortRegistration: formData.shortRegistration,
|
||||||
swimmingPermissionRequired: formData.swimmingPermissionRequired,
|
swimmingPermissionRequired: formData.swimmingPermissionRequired,
|
||||||
|
cocCheckRequired: formData.cocCheckRequired,
|
||||||
flatSupport: formData.flatSupport,
|
flatSupport: formData.flatSupport,
|
||||||
supportPerson: formData.supportPerson,
|
supportPerson: formData.supportPerson,
|
||||||
contributingLocalGroups: contributingLocalGroups.value,
|
contributingLocalGroups: contributingLocalGroups.value,
|
||||||
eatingHabits: eatingHabits.value,
|
eatingHabits: eatingHabits.value,
|
||||||
|
participantGroupType: formData.participantGroupType,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -173,6 +182,18 @@ const emit = defineEmits(['close'])
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th>Bei der Anmeldung abfragen</th>
|
||||||
|
<td>
|
||||||
|
<select v-model="formData.participantGroupType" class="width-full" :disabled="participantGroupTypeLocked">
|
||||||
|
<option v-for="type in dynmicProps.participantGroupTypes" :key="type.value" :value="type.value">{{ type.label }}</option>
|
||||||
|
</select>
|
||||||
|
<span v-if="participantGroupTypeLocked" style="font-size: 0.8rem; color: #6b7280;">
|
||||||
|
Nicht mehr änderbar, da bereits Anmeldungen vorliegen.
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<th>Mindestalter für Alkoholkonsum</th>
|
<th>Mindestalter für Alkoholkonsum</th>
|
||||||
<td>
|
<td>
|
||||||
@@ -215,6 +236,16 @@ const emit = defineEmits(['close'])
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td colspan="2">
|
||||||
|
<input type="checkbox" v-model="formData.cocCheckRequired" id="cocCheckRequired" />
|
||||||
|
<label for="cocCheckRequired">Erweitertes Führungszeugnis (eFZ) prüfen</label><br />
|
||||||
|
<span style="font-size: 0.8rem; color: #6b7280;">
|
||||||
|
Ist die Prüfung deaktiviert, wird das eFZ bei neuen Anmeldungen als „Nicht erforderlich" hinterlegt.
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|
||||||
@@ -223,7 +254,7 @@ const emit = defineEmits(['close'])
|
|||||||
<div class="right">
|
<div class="right">
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Teilnehmende Stämme</th>
|
<th>Teilnehmende {{ dynmicProps.localGroupLabelPlural }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-for="localGroup in dynmicProps.localGroups">
|
<tr v-for="localGroup in dynmicProps.localGroups">
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const form = reactive({
|
|||||||
address_2: '',
|
address_2: '',
|
||||||
postcode: '',
|
postcode: '',
|
||||||
city: '',
|
city: '',
|
||||||
localgroup: '',
|
participantGroup: '',
|
||||||
birthday: '',
|
birthday: '',
|
||||||
email_1: '',
|
email_1: '',
|
||||||
phone_1: '',
|
phone_1: '',
|
||||||
@@ -71,6 +71,9 @@ const form = reactive({
|
|||||||
// Teilnahmeoptionen des Events (Definition mit Titel/Frage/Optionen).
|
// Teilnahmeoptionen des Events (Definition mit Titel/Frage/Optionen).
|
||||||
const participationQuestions = computed(() => staticProps.event?.participationOptions ?? []);
|
const participationQuestions = computed(() => staticProps.event?.participationOptions ?? []);
|
||||||
|
|
||||||
|
// Bezeichnung der gewählten Gruppe (z. B. „Stamm" oder „Bund"), je nach Einstellung der Veranstaltung.
|
||||||
|
const localGroupLabel = computed(() => staticProps.event?.participantGroupLabel ?? 'Stamm');
|
||||||
|
|
||||||
// Zusatzoptionen (Event-Addons) des Events.
|
// Zusatzoptionen (Event-Addons) des Events.
|
||||||
const eventAddons = computed(() => staticProps.event?.addons ?? []);
|
const eventAddons = computed(() => staticProps.event?.addons ?? []);
|
||||||
|
|
||||||
@@ -95,7 +98,7 @@ watch(
|
|||||||
form.address_2 = participant?.address_2 ?? '';
|
form.address_2 = participant?.address_2 ?? '';
|
||||||
form.postcode = participant?.postcode ?? '';
|
form.postcode = participant?.postcode ?? '';
|
||||||
form.city = participant?.city ?? '';
|
form.city = participant?.city ?? '';
|
||||||
form.localgroup = participant?.local_group ?? '';
|
form.participantGroup = participant?.participantGroupValue ?? '';
|
||||||
form.birthday = participant?.birthdayDate ?? '';
|
form.birthday = participant?.birthdayDate ?? '';
|
||||||
form.email_1 = participant?.email_1 ?? '';
|
form.email_1 = participant?.email_1 ?? '';
|
||||||
form.phone_1 = participant?.phone_1 ?? '';
|
form.phone_1 = participant?.phone_1 ?? '';
|
||||||
@@ -229,12 +232,12 @@ function saveParticipant() {
|
|||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<th>Stamm</th>
|
<th>{{ localGroupLabel }}</th>
|
||||||
<td>
|
<td>
|
||||||
<span v-if="!staticProps.editMode">{{ props.participant.localgroup }}</span>
|
<span v-if="!staticProps.editMode">{{ props.participant.participantGroup }}</span>
|
||||||
<select v-else v-model="form.localgroup">
|
<select v-else v-model="form.participantGroup">
|
||||||
<option value="">Kein Stamm</option>
|
<option value="">Keine Angabe</option>
|
||||||
<option v-for="group in staticProps.event.contributingLocalGroups" :key="group.id" :value="group.slug">{{ group.name }}</option>
|
<option v-for="group in staticProps.event.participantGroupOptions" :key="group.id" :value="group.value">{{ group.name }}</option>
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ async function saveParticipant(formData) {
|
|||||||
document.getElementById('participant-' + data.participant.identifier + '-fullname').innerHTML = data.participant.fullname;
|
document.getElementById('participant-' + data.participant.identifier + '-fullname').innerHTML = data.participant.fullname;
|
||||||
document.getElementById('participant-' + data.participant.identifier + '-birthday').innerText = data.participant.birthday;
|
document.getElementById('participant-' + data.participant.identifier + '-birthday').innerText = data.participant.birthday;
|
||||||
document.getElementById('participant-' + data.participant.identifier + '-age').innerText = data.participant.age;
|
document.getElementById('participant-' + data.participant.identifier + '-age').innerText = data.participant.age;
|
||||||
document.getElementById('participant-' + data.participant.identifier + '-localgroup').innerText = data.participant.localgroup;
|
document.getElementById('participant-' + data.participant.identifier + '-localgroup').innerText = data.participant.participantGroup;
|
||||||
document.getElementById('participant-' + data.participant.identifier + '-arrival').innerText = data.participant.arrival;
|
document.getElementById('participant-' + data.participant.identifier + '-arrival').innerText = data.participant.arrival;
|
||||||
document.getElementById('participant-' + data.participant.identifier + '-departure').innerText = data.participant.departure;
|
document.getElementById('participant-' + data.participant.identifier + '-departure').innerText = data.participant.departure;
|
||||||
document.getElementById('participant-' + data.participant.identifier + '-email_1').innerText = data.participant.email_1;
|
document.getElementById('participant-' + data.participant.identifier + '-email_1').innerText = data.participant.email_1;
|
||||||
@@ -641,7 +641,7 @@ function mailToGroup(groupKey) {
|
|||||||
<tr class="participant-meta-row" :id="'participant-' + participant.identifier + '-meta'">
|
<tr class="participant-meta-row" :id="'participant-' + participant.identifier + '-meta'">
|
||||||
<td colspan="5" class="pl-meta">
|
<td colspan="5" class="pl-meta">
|
||||||
<label :id="'participant-' + participant.identifier +'-localgroup'">
|
<label :id="'participant-' + participant.identifier +'-localgroup'">
|
||||||
{{ participant?.localgroup ?? "-" }}
|
{{ participant?.participantGroup ?? "-" }}
|
||||||
</label> |
|
</label> |
|
||||||
<strong> Anreise: </strong>
|
<strong> Anreise: </strong>
|
||||||
<label :id="'participant-' + participant.identifier +'-arrival'">
|
<label :id="'participant-' + participant.identifier +'-arrival'">
|
||||||
|
|||||||
@@ -401,6 +401,9 @@ onMounted(async () => {
|
|||||||
placeholder="Symbol wählen…"/>
|
placeholder="Symbol wählen…"/>
|
||||||
<TextEditor v-else-if="option.type === 'richtext'"
|
<TextEditor v-else-if="option.type === 'richtext'"
|
||||||
v-model="paymentMethodConfigurations[optionsMethod.slug][option.name]"/>
|
v-model="paymentMethodConfigurations[optionsMethod.slug][option.name]"/>
|
||||||
|
<input v-else-if="option.type === 'number'" type="number" step="1"
|
||||||
|
v-model="paymentMethodConfigurations[optionsMethod.slug][option.name]"
|
||||||
|
class="width-full"/>
|
||||||
<input v-else type="text"
|
<input v-else type="text"
|
||||||
v-model="paymentMethodConfigurations[optionsMethod.slug][option.name]"
|
v-model="paymentMethodConfigurations[optionsMethod.slug][option.name]"
|
||||||
class="width-full"/>
|
class="width-full"/>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {computed} from 'vue'
|
|||||||
import {useShortSignupForm} from './composables/useShortSignupForm.js'
|
import {useShortSignupForm} from './composables/useShortSignupForm.js'
|
||||||
import {
|
import {
|
||||||
SHORT_STEP_PERSON,
|
SHORT_STEP_PERSON,
|
||||||
|
SHORT_STEP_FEE,
|
||||||
SHORT_STEP_PARTICIPATION_OPTIONS,
|
SHORT_STEP_PARTICIPATION_OPTIONS,
|
||||||
SHORT_STEP_PHOTO,
|
SHORT_STEP_PHOTO,
|
||||||
SHORT_STEP_PAYMENT,
|
SHORT_STEP_PAYMENT,
|
||||||
@@ -10,6 +11,7 @@ import {
|
|||||||
stepVisible,
|
stepVisible,
|
||||||
} from './composables/shortStepFlow.js'
|
} from './composables/shortStepFlow.js'
|
||||||
import ShortStepPerson from './steps/ShortStepPerson.vue'
|
import ShortStepPerson from './steps/ShortStepPerson.vue'
|
||||||
|
import ShortStepFee from './steps/ShortStepFee.vue'
|
||||||
import ShortStepSummary from './steps/ShortStepSummary.vue'
|
import ShortStepSummary from './steps/ShortStepSummary.vue'
|
||||||
import StepParticipationOptions from '../SignUpForm/steps/StepParticipationOptions.vue'
|
import StepParticipationOptions from '../SignUpForm/steps/StepParticipationOptions.vue'
|
||||||
import StepPhotoPermissions from '../SignUpForm/steps/StepPhotoPermissions.vue'
|
import StepPhotoPermissions from '../SignUpForm/steps/StepPhotoPermissions.vue'
|
||||||
@@ -25,7 +27,7 @@ const props = defineProps({
|
|||||||
const {
|
const {
|
||||||
currentStep, goToStep, goNext, goBack, formData, isMinor, localGroups, needsLocalGroup,
|
currentStep, goToStep, goNext, goBack, formData, isMinor, localGroups, needsLocalGroup,
|
||||||
submit, submitting, submitResult, submitError,
|
submit, submitting, submitResult, submitError,
|
||||||
summaryLoading, summaryAmount, summaryAmountValue,
|
summaryLoading, summaryAmount, feeOptions, selectedFee, flowState,
|
||||||
} = useShortSignupForm(props.event, props.participantData)
|
} = useShortSignupForm(props.event, props.participantData)
|
||||||
|
|
||||||
// Die aus dem langen Prozess übernommenen Schritte emittieren ihre Zielnummer aus jenem Flow mit. Hier zählt
|
// Die aus dem langen Prozess übernommenen Schritte emittieren ihre Zielnummer aus jenem Flow mit. Hier zählt
|
||||||
@@ -35,6 +37,7 @@ const onBack = () => goBack()
|
|||||||
|
|
||||||
const allSteps = [
|
const allSteps = [
|
||||||
{step: SHORT_STEP_PERSON, label: 'Person'},
|
{step: SHORT_STEP_PERSON, label: 'Person'},
|
||||||
|
{step: SHORT_STEP_FEE, label: 'Beitrag'},
|
||||||
{step: SHORT_STEP_PARTICIPATION_OPTIONS, label: 'Teilnahmeoptionen'},
|
{step: SHORT_STEP_PARTICIPATION_OPTIONS, label: 'Teilnahmeoptionen'},
|
||||||
{step: SHORT_STEP_PHOTO, label: 'Fotoerlaubnis'},
|
{step: SHORT_STEP_PHOTO, label: 'Fotoerlaubnis'},
|
||||||
{step: SHORT_STEP_PAYMENT, label: 'Zahlungsart'},
|
{step: SHORT_STEP_PAYMENT, label: 'Zahlungsart'},
|
||||||
@@ -43,7 +46,7 @@ const allSteps = [
|
|||||||
|
|
||||||
// Nur die tatsächlich vorkommenden Schritte anzeigen, sonst zeigt die Leiste Schritte, die nie erscheinen.
|
// Nur die tatsächlich vorkommenden Schritte anzeigen, sonst zeigt die Leiste Schritte, die nie erscheinen.
|
||||||
const steps = computed(() =>
|
const steps = computed(() =>
|
||||||
allSteps.filter(s => stepVisible(props.event, s.step, summaryAmountValue.value))
|
allSteps.filter(s => stepVisible(props.event, s.step, flowState.value))
|
||||||
)
|
)
|
||||||
|
|
||||||
const currentIndex = computed(() => steps.value.findIndex(s => s.step === currentStep.value))
|
const currentIndex = computed(() => steps.value.findIndex(s => s.step === currentStep.value))
|
||||||
@@ -101,6 +104,11 @@ const currentIndex = computed(() => steps.value.findIndex(s => s.step === curren
|
|||||||
:localGroups="localGroups" :needsLocalGroup="needsLocalGroup"
|
:localGroups="localGroups" :needsLocalGroup="needsLocalGroup"
|
||||||
@next="onNext"
|
@next="onNext"
|
||||||
/>
|
/>
|
||||||
|
<ShortStepFee
|
||||||
|
v-if="currentStep === SHORT_STEP_FEE"
|
||||||
|
:formData="formData" :feeOptions="feeOptions"
|
||||||
|
@next="onNext" @back="onBack"
|
||||||
|
/>
|
||||||
<StepParticipationOptions
|
<StepParticipationOptions
|
||||||
v-if="currentStep === SHORT_STEP_PARTICIPATION_OPTIONS"
|
v-if="currentStep === SHORT_STEP_PARTICIPATION_OPTIONS"
|
||||||
:formData="formData" :event="event"
|
:formData="formData" :event="event"
|
||||||
@@ -122,7 +130,7 @@ const currentIndex = computed(() => steps.value.findIndex(s => s.step === curren
|
|||||||
:event="event"
|
:event="event"
|
||||||
:isMinor="isMinor"
|
:isMinor="isMinor"
|
||||||
:summaryAmount="summaryAmount"
|
:summaryAmount="summaryAmount"
|
||||||
:summaryAmountValue="summaryAmountValue"
|
:feeLabel="feeOptions.length > 1 ? selectedFee?.label : null"
|
||||||
:summaryLoading="summaryLoading"
|
:summaryLoading="summaryLoading"
|
||||||
:submitting="submitting"
|
:submitting="submitting"
|
||||||
:submitError="submitError"
|
:submitError="submitError"
|
||||||
|
|||||||
@@ -1,36 +1,43 @@
|
|||||||
// Schrittreihenfolge der Kurzanmeldung. Zwei Schritte sind optional: die Teilnahmeoptionen entfallen, wenn für
|
// Schrittreihenfolge der Kurzanmeldung. Drei Schritte sind optional: der Beitrag erscheint nur, wenn neben dem
|
||||||
|
// Standardbeitrag weitere Stufen (reduziert, Solidarität) hinterlegt sind, die Teilnahmeoptionen entfallen, wenn für
|
||||||
// die Veranstaltung keine definiert sind, die Zahlungsart entfällt bei Betrag 0 oder wenn es ohnehin nur eine
|
// die Veranstaltung keine definiert sind, die Zahlungsart entfällt bei Betrag 0 oder wenn es ohnehin nur eine
|
||||||
// Zahlungsart gibt (die wird dann vorausgewählt).
|
// Zahlungsart gibt (die wird dann vorausgewählt).
|
||||||
export const SHORT_STEP_PERSON = 1
|
export const SHORT_STEP_PERSON = 1
|
||||||
export const SHORT_STEP_PARTICIPATION_OPTIONS = 2
|
export const SHORT_STEP_FEE = 2
|
||||||
export const SHORT_STEP_PHOTO = 3
|
export const SHORT_STEP_PARTICIPATION_OPTIONS = 3
|
||||||
export const SHORT_STEP_PAYMENT = 4
|
export const SHORT_STEP_PHOTO = 4
|
||||||
export const SHORT_STEP_SUMMARY = 5
|
export const SHORT_STEP_PAYMENT = 5
|
||||||
|
export const SHORT_STEP_SUMMARY = 6
|
||||||
|
|
||||||
|
// `state` kommt mit den Beiträgen vom Server: { amountValue: Betrag der gewählten Stufe, feeOptionCount }.
|
||||||
|
export function stepVisible(event, step, state) {
|
||||||
|
if (step === SHORT_STEP_FEE) {
|
||||||
|
return state.feeOptionCount > 1
|
||||||
|
}
|
||||||
|
|
||||||
export function stepVisible(event, step, amountValue) {
|
|
||||||
if (step === SHORT_STEP_PARTICIPATION_OPTIONS) {
|
if (step === SHORT_STEP_PARTICIPATION_OPTIONS) {
|
||||||
return (event.participationOptions?.length ?? 0) > 0
|
return (event.participationOptions?.length ?? 0) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if (step === SHORT_STEP_PAYMENT) {
|
if (step === SHORT_STEP_PAYMENT) {
|
||||||
const activeMethods = (event.paymentMethods ?? []).filter(m => m.active)
|
const activeMethods = (event.paymentMethods ?? []).filter(m => m.active)
|
||||||
return amountValue > 0 && activeMethods.length > 1
|
return state.amountValue > 0 && activeMethods.length > 1
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
export function nextVisibleFrom(event, step, amountValue) {
|
export function nextVisibleFrom(event, step, state) {
|
||||||
let candidate = step + 1
|
let candidate = step + 1
|
||||||
while (candidate < SHORT_STEP_SUMMARY && !stepVisible(event, candidate, amountValue)) {
|
while (candidate < SHORT_STEP_SUMMARY && !stepVisible(event, candidate, state)) {
|
||||||
candidate++
|
candidate++
|
||||||
}
|
}
|
||||||
return candidate
|
return candidate
|
||||||
}
|
}
|
||||||
|
|
||||||
export function prevVisibleFrom(event, step, amountValue) {
|
export function prevVisibleFrom(event, step, state) {
|
||||||
let candidate = step - 1
|
let candidate = step - 1
|
||||||
while (candidate > SHORT_STEP_PERSON && !stepVisible(event, candidate, amountValue)) {
|
while (candidate > SHORT_STEP_PERSON && !stepVisible(event, candidate, state)) {
|
||||||
candidate--
|
candidate--
|
||||||
}
|
}
|
||||||
return candidate
|
return candidate
|
||||||
|
|||||||
+42
-18
@@ -1,4 +1,4 @@
|
|||||||
import {computed, reactive, ref} from 'vue'
|
import {computed, reactive, ref, watch} from 'vue'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import {differenceInYears, parseISO} from 'date-fns'
|
import {differenceInYears, parseISO} from 'date-fns'
|
||||||
import {SHORT_STEP_PERSON, nextVisibleFrom, prevVisibleFrom} from './shortStepFlow.js'
|
import {SHORT_STEP_PERSON, nextVisibleFrom, prevVisibleFrom} from './shortStepFlow.js'
|
||||||
@@ -13,10 +13,12 @@ export function useShortSignupForm(event, participantData) {
|
|||||||
// Gibt es nur eine Zahlungsart, wird sie vorausgewählt und der Zahlungsschritt entfällt.
|
// Gibt es nur eine Zahlungsart, wird sie vorausgewählt und der Zahlungsschritt entfällt.
|
||||||
const activeMethods = (event.paymentMethods ?? []).filter(m => m.active)
|
const activeMethods = (event.paymentMethods ?? []).filter(m => m.active)
|
||||||
|
|
||||||
// Bei genau einem teilnehmenden Stamm wird dieser gesetzt, ohne danach zu fragen; erst ab zwei
|
// Auswahl je nach Veranstaltung: teilnehmende Stämme oder deren Bünde. Bei genau einer Option wird diese
|
||||||
// Stämmen erscheint das Auswahlfeld.
|
// gesetzt, ohne danach zu fragen; erst ab zwei Optionen erscheint das Auswahlfeld.
|
||||||
const localGroups = event.contributingLocalGroups ?? []
|
const localGroups = event.participantGroupOptions ?? []
|
||||||
const needsLocalGroup = localGroups.length > 1
|
const needsLocalGroup = localGroups.length > 1
|
||||||
|
// Vorbelegung mit dem Stamm des eingeloggten Nutzers nur, wenn er zur Auswahl gehört (bei Bund-Abfrage nie).
|
||||||
|
const preselectedGroup = localGroups.some(g => g.id === participantData.localGroup) ? participantData.localGroup : '-1'
|
||||||
|
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
userId: participantData.id,
|
userId: participantData.id,
|
||||||
@@ -26,13 +28,15 @@ export function useShortSignupForm(event, participantData) {
|
|||||||
geburtsdatum: participantData.birthday ?? '',
|
geburtsdatum: participantData.birthday ?? '',
|
||||||
email_1: participantData.email ?? '',
|
email_1: participantData.email ?? '',
|
||||||
telefon_1: participantData.phone ?? '',
|
telefon_1: participantData.phone ?? '',
|
||||||
localGroup: localGroups.length === 1 ? localGroups[0].id : (participantData.localGroup ?? '-1'),
|
localGroup: localGroups.length === 1 ? localGroups[0].id : preselectedGroup,
|
||||||
ansprechpartner: '',
|
ansprechpartner: '',
|
||||||
email_2: '',
|
email_2: '',
|
||||||
allergien: participantData.allergies ?? '',
|
allergien: participantData.allergies ?? '',
|
||||||
intolerances: participantData.intolerances ?? '',
|
intolerances: participantData.intolerances ?? '',
|
||||||
first_aid: '-1',
|
first_aid: '-1',
|
||||||
badeerlaubnis: '-1',
|
badeerlaubnis: '-1',
|
||||||
|
// Beitragsstufe; wählbar nur, wenn weitere Stufen hinterlegt sind.
|
||||||
|
feeType: 'standard',
|
||||||
// Antworten auf die event-spezifischen Teilnahmeoptionen: { [question.key]: option.value }
|
// Antworten auf die event-spezifischen Teilnahmeoptionen: { [question.key]: option.value }
|
||||||
participationOptions: {},
|
participationOptions: {},
|
||||||
foto: {socialmedia: false, print: false, webseite: false, partner: false, intern: false},
|
foto: {socialmedia: false, print: false, webseite: false, partner: false, intern: false},
|
||||||
@@ -53,12 +57,18 @@ export function useShortSignupForm(event, participantData) {
|
|||||||
return differenceInYears(new Date(), parseISO(formData.geburtsdatum)) < 18
|
return differenceInYears(new Date(), parseISO(formData.geburtsdatum)) < 18
|
||||||
})
|
})
|
||||||
|
|
||||||
const summaryAmount = ref('')
|
// Beitragsstufen mit fertig berechnetem Betrag: [{ value, label, amount, amountValue }]
|
||||||
const summaryAmountValue = ref(0)
|
const feeOptions = ref([])
|
||||||
const amountLoaded = ref(false)
|
const amountLoaded = ref(false)
|
||||||
|
|
||||||
// Der Betrag steht für die ganze Anmeldung fest (feste Teilnahmegruppe, fester Zeitraum) und wird deshalb
|
const selectedFee = computed(() =>
|
||||||
// genau einmal geholt. Er entscheidet, ob der Zahlungsschritt überhaupt vorkommt.
|
feeOptions.value.find(option => option.value === formData.feeType) ?? feeOptions.value[0] ?? null
|
||||||
|
)
|
||||||
|
const summaryAmount = computed(() => selectedFee.value?.amount ?? '')
|
||||||
|
const summaryAmountValue = computed(() => Number(selectedFee.value?.amountValue ?? 0))
|
||||||
|
|
||||||
|
// Teilnahmegruppe und Zeitraum stehen für die ganze Anmeldung fest, deshalb werden die Beträge aller Stufen
|
||||||
|
// genau einmal geholt. Die Wahl der Stufe entscheidet dann nur noch lokal über den Betrag.
|
||||||
const ensureAmount = async () => {
|
const ensureAmount = async () => {
|
||||||
if (amountLoaded.value) {
|
if (amountLoaded.value) {
|
||||||
return
|
return
|
||||||
@@ -67,31 +77,41 @@ export function useShortSignupForm(event, participantData) {
|
|||||||
summaryLoading.value = true
|
summaryLoading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await axios.post('/api/v1/event/' + event.id + '/short-calculate-amount')
|
const res = await axios.post('/api/v1/event/' + event.id + '/short-calculate-amount')
|
||||||
summaryAmount.value = res.data.amount
|
feeOptions.value = res.data.feeTypes ?? []
|
||||||
summaryAmountValue.value = Number(res.data.amountValue ?? 0)
|
|
||||||
amountLoaded.value = true
|
amountLoaded.value = true
|
||||||
|
|
||||||
if (summaryAmountValue.value <= 0) {
|
|
||||||
formData.paymentMethod = null
|
|
||||||
formData.paymentOptions = {}
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
summaryLoading.value = false
|
summaryLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bei Betrag 0 gibt es nichts zu bezahlen. Wechselt die Stufe zurück auf einen Betrag, wird die einzige
|
||||||
|
// Zahlungsart wieder vorausgewählt -- sonst fragt sie niemand mehr ab.
|
||||||
|
watch(summaryAmountValue, (value) => {
|
||||||
|
if (value <= 0) {
|
||||||
|
formData.paymentMethod = null
|
||||||
|
formData.paymentOptions = {}
|
||||||
|
} else if (formData.paymentMethod === null && activeMethods.length === 1) {
|
||||||
|
formData.paymentMethod = activeMethods[0].slug
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const flowState = computed(() => ({
|
||||||
|
amountValue: summaryAmountValue.value,
|
||||||
|
feeOptionCount: feeOptions.value.length,
|
||||||
|
}))
|
||||||
|
|
||||||
const goToStep = (step) => {
|
const goToStep = (step) => {
|
||||||
currentStep.value = step
|
currentStep.value = step
|
||||||
}
|
}
|
||||||
|
|
||||||
const goNext = async () => {
|
const goNext = async () => {
|
||||||
await ensureAmount()
|
await ensureAmount()
|
||||||
goToStep(nextVisibleFrom(event, currentStep.value, summaryAmountValue.value))
|
goToStep(nextVisibleFrom(event, currentStep.value, flowState.value))
|
||||||
}
|
}
|
||||||
|
|
||||||
const goBack = async () => {
|
const goBack = async () => {
|
||||||
await ensureAmount()
|
await ensureAmount()
|
||||||
goToStep(prevVisibleFrom(event, currentStep.value, summaryAmountValue.value))
|
goToStep(prevVisibleFrom(event, currentStep.value, flowState.value))
|
||||||
}
|
}
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
@@ -120,6 +140,7 @@ export function useShortSignupForm(event, participantData) {
|
|||||||
participationOptions: formData.participationOptions,
|
participationOptions: formData.participationOptions,
|
||||||
paymentMethod: formData.paymentMethod,
|
paymentMethod: formData.paymentMethod,
|
||||||
paymentOptions: formData.paymentOptions,
|
paymentOptions: formData.paymentOptions,
|
||||||
|
feeType: formData.feeType,
|
||||||
})
|
})
|
||||||
|
|
||||||
submitResult.value = {
|
submitResult.value = {
|
||||||
@@ -150,6 +171,9 @@ export function useShortSignupForm(event, participantData) {
|
|||||||
summaryLoading,
|
summaryLoading,
|
||||||
summaryAmount,
|
summaryAmount,
|
||||||
summaryAmountValue,
|
summaryAmountValue,
|
||||||
|
feeOptions,
|
||||||
|
selectedFee,
|
||||||
|
flowState,
|
||||||
ensureAmount,
|
ensureAmount,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<script setup>
|
||||||
|
// Erscheint nur, wenn neben dem Standardbeitrag weitere Stufen hinterlegt sind. Die Beträge gelten für die gesamte
|
||||||
|
// Veranstaltung -- die Kurzanmeldung hat keinen wählbaren Zeitraum.
|
||||||
|
defineProps({
|
||||||
|
formData: Object,
|
||||||
|
feeOptions: {type: Array, default: () => []},
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['next', 'back'])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h3>Teilnahmebeitrag</h3>
|
||||||
|
<p style="margin: 0 0 16px 0; color: #6b7280; font-size: 0.95rem;">
|
||||||
|
Bitte wähle deinen Beitrag für die gesamte Veranstaltung.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<label
|
||||||
|
v-for="option in feeOptions"
|
||||||
|
:key="option.value"
|
||||||
|
style="display: block; margin-bottom: 8px; cursor: pointer;"
|
||||||
|
>
|
||||||
|
<input type="radio" v-model="formData.feeType" :value="option.value" />
|
||||||
|
{{ option.label }}
|
||||||
|
<span style="color: #606060;">({{ option.amount }})</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="btn-row">
|
||||||
|
<button type="button" class="btn-secondary" @click="emit('back')">← Zurück</button>
|
||||||
|
<button type="button" class="btn-primary" @click="emit('next')">Weiter →</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -11,6 +11,9 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
const emit = defineEmits(['next'])
|
const emit = defineEmits(['next'])
|
||||||
|
|
||||||
|
// Bezeichnung der Auswahl (z. B. „Stamm" oder „Bund"), je nach Einstellung der Veranstaltung.
|
||||||
|
const localGroupLabel = props.event?.participantGroupLabel ?? 'Stamm'
|
||||||
|
|
||||||
const errors = reactive({
|
const errors = reactive({
|
||||||
vorname: '',
|
vorname: '',
|
||||||
nachname: '',
|
nachname: '',
|
||||||
@@ -46,7 +49,7 @@ const next = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (props.needsLocalGroup && (!props.formData.localGroup || props.formData.localGroup === '-1')) {
|
if (props.needsLocalGroup && (!props.formData.localGroup || props.formData.localGroup === '-1')) {
|
||||||
errors.localGroup = 'Bitte einen Stamm auswählen.'
|
errors.localGroup = 'Bitte bei „' + localGroupLabel + '" eine Auswahl treffen.'
|
||||||
hasError = true
|
hasError = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +125,7 @@ const next = () => {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="needsLocalGroup">
|
<tr v-if="needsLocalGroup">
|
||||||
<td>Stamm:</td>
|
<td>{{ localGroupLabel }}:</td>
|
||||||
<td>
|
<td>
|
||||||
<select v-model="formData.localGroup">
|
<select v-model="formData.localGroup">
|
||||||
<option value="-1">Bitte wählen</option>
|
<option value="-1">Bitte wählen</option>
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ const props = defineProps({
|
|||||||
event: Object,
|
event: Object,
|
||||||
isMinor: Boolean,
|
isMinor: Boolean,
|
||||||
summaryAmount: String,
|
summaryAmount: String,
|
||||||
summaryAmountValue: {type: Number, default: 0},
|
// Nur gesetzt, wenn zwischen mehreren Beitragsstufen gewählt werden konnte.
|
||||||
|
feeLabel: {type: String, default: null},
|
||||||
summaryLoading: Boolean,
|
summaryLoading: Boolean,
|
||||||
submitting: Boolean,
|
submitting: Boolean,
|
||||||
submitError: {type: String, default: ''},
|
submitError: {type: String, default: ''},
|
||||||
@@ -42,9 +43,9 @@ const selectedParticipationOptions = computed(() =>
|
|||||||
}).filter(entry => entry.value !== '')
|
}).filter(entry => entry.value !== '')
|
||||||
)
|
)
|
||||||
|
|
||||||
// Der Stamm steht entweder fest (genau ein teilnehmender Stamm) oder wurde auf Seite 1 gewählt.
|
// Die Gruppe (Stamm oder Bund) steht entweder fest (genau eine Option) oder wurde auf Seite 1 gewählt.
|
||||||
const localGroupName = computed(() =>
|
const localGroupName = computed(() =>
|
||||||
(props.event.contributingLocalGroups ?? []).find(g => g.id === props.formData.localGroup)?.name ?? null
|
(props.event.participantGroupOptions ?? []).find(g => g.id === props.formData.localGroup)?.name ?? null
|
||||||
)
|
)
|
||||||
|
|
||||||
const permissionName = (list, slug) =>
|
const permissionName = (list, slug) =>
|
||||||
@@ -83,7 +84,7 @@ const canSubmit = computed(() =>
|
|||||||
<td>{{ formData.telefon_1 }}</td>
|
<td>{{ formData.telefon_1 }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="localGroupName">
|
<tr v-if="localGroupName">
|
||||||
<td>Stamm:</td>
|
<td>{{ event.participantGroupLabel ?? 'Stamm' }}:</td>
|
||||||
<td>{{ localGroupName }}</td>
|
<td>{{ localGroupName }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
@@ -137,6 +138,10 @@ const canSubmit = computed(() =>
|
|||||||
|
|
||||||
<h4 style="margin: 0 0 8px 0;">Kostenübersicht</h4>
|
<h4 style="margin: 0 0 8px 0;">Kostenübersicht</h4>
|
||||||
<table class="form-table cost-table" style="margin-bottom: 20px;">
|
<table class="form-table cost-table" style="margin-bottom: 20px;">
|
||||||
|
<tr v-if="feeLabel">
|
||||||
|
<td>Beitragsstufe:</td>
|
||||||
|
<td>{{ feeLabel }}</td>
|
||||||
|
</tr>
|
||||||
<tr class="cost-total">
|
<tr class="cost-total">
|
||||||
<td><strong>Teilnahmebeitrag:</strong></td>
|
<td><strong>Teilnahmebeitrag:</strong></td>
|
||||||
<td><strong>{{ summaryAmount }}</strong></td>
|
<td><strong>{{ summaryAmount }}</strong></td>
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ const steps = [
|
|||||||
<form @submit.prevent="submit">
|
<form @submit.prevent="submit">
|
||||||
<StepAge v-if="currentStep === 1" :event="event" @next="goToStep" />
|
<StepAge v-if="currentStep === 1" :event="event" @next="goToStep" />
|
||||||
<StepContactPerson v-if="currentStep === 2" :formData="formData" :event="event" @next="goToStep" @back="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" />
|
<StepPersonalData v-if="currentStep === 3" :formData="formData" :event="event" :localGroups="localGroups" @next="goToStep" @back="goToStep" />
|
||||||
<StepArrival v-if="currentStep === 4" :formData="formData" :event="event" @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" />
|
<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" />
|
<StepAddons v-if="currentStep === 6" :formData="formData" :event="event" :selectedAddons="selectedAddons" @next="goToStep" @back="goToStep" />
|
||||||
|
|||||||
@@ -13,8 +13,13 @@ export function useSignupForm(event, participantData) {
|
|||||||
// Für die Aktion freigeschaltete Zahlungsmethoden; ist genau eine aktiv, wird sie vorausgewählt.
|
// Für die Aktion freigeschaltete Zahlungsmethoden; ist genau eine aktiv, wird sie vorausgewählt.
|
||||||
const activeMethods = (event.paymentMethods ?? []).filter(m => m.active)
|
const activeMethods = (event.paymentMethods ?? []).filter(m => m.active)
|
||||||
|
|
||||||
|
// Vorbelegung mit der Ernährungsweise aus dem Profil, sofern die Veranstaltung sie anbietet, sonst die erste Option.
|
||||||
|
const eatingHabitSlugs = (event.eatingHabits ?? []).map(h => h.data.slug)
|
||||||
|
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
eatingHabit: 'EATING_HABIT_VEGAN',
|
eatingHabit: eatingHabitSlugs.includes(participantData.eating_habit)
|
||||||
|
? participantData.eating_habit
|
||||||
|
: (eatingHabitSlugs[0] ?? ''),
|
||||||
paymentMethod: activeMethods.length === 1 ? activeMethods[0].slug : null,
|
paymentMethod: activeMethods.length === 1 ? activeMethods[0].slug : null,
|
||||||
paymentOptions: {},
|
paymentOptions: {},
|
||||||
// Antworten auf die event-spezifischen Teilnahmeoptionen: { [question.key]: option.value }
|
// Antworten auf die event-spezifischen Teilnahmeoptionen: { [question.key]: option.value }
|
||||||
@@ -24,7 +29,10 @@ export function useSignupForm(event, participantData) {
|
|||||||
vorname: participantData.firstname ?? '',
|
vorname: participantData.firstname ?? '',
|
||||||
nachname: participantData.lastname ?? '',
|
nachname: participantData.lastname ?? '',
|
||||||
pfadiname: participantData.nickname ?? '',
|
pfadiname: participantData.nickname ?? '',
|
||||||
localGroup: participantData.localGroup ?? '-1',
|
// Vorbelegung mit dem Stamm des eingeloggten Nutzers nur, wenn er zur Auswahl gehört (bei Bund-Abfrage nie).
|
||||||
|
localGroup: (event.participantGroupOptions ?? []).some(g => g.id === participantData.localGroup)
|
||||||
|
? participantData.localGroup
|
||||||
|
: '-1',
|
||||||
geburtsdatum: participantData.birthday ?? '',
|
geburtsdatum: participantData.birthday ?? '',
|
||||||
address1: participantData.address_1 ?? '',
|
address1: participantData.address_1 ?? '',
|
||||||
address2: participantData.address_2 ?? '',
|
address2: participantData.address_2 ?? '',
|
||||||
@@ -50,7 +58,6 @@ export function useSignupForm(event, participantData) {
|
|||||||
intolerances: participantData.intolerances ?? '',
|
intolerances: participantData.intolerances ?? '',
|
||||||
medikamente: participantData.medications ?? '',
|
medikamente: participantData.medications ?? '',
|
||||||
tetanusVaccination: participantData.tetanusVaccination ?? '',
|
tetanusVaccination: participantData.tetanusVaccination ?? '',
|
||||||
essgewohnheit: 'vegetarian',
|
|
||||||
anmerkungen: '',
|
anmerkungen: '',
|
||||||
summary_information_correct: false,
|
summary_information_correct: false,
|
||||||
summary_accept_terms: false,
|
summary_accept_terms: false,
|
||||||
|
|||||||
@@ -2,9 +2,12 @@
|
|||||||
import {reactive} from "vue";
|
import {reactive} from "vue";
|
||||||
import ErrorText from "../../../../../../Views/Components/ErrorText.vue";
|
import ErrorText from "../../../../../../Views/Components/ErrorText.vue";
|
||||||
|
|
||||||
const props = defineProps({ formData: Object, localGroups: Array })
|
const props = defineProps({ formData: Object, event: Object, localGroups: Array })
|
||||||
const emit = defineEmits(['next', 'back'])
|
const emit = defineEmits(['next', 'back'])
|
||||||
|
|
||||||
|
// Bezeichnung der Auswahl (z. B. „Stamm" oder „Bund"), je nach Einstellung der Veranstaltung.
|
||||||
|
const localGroupLabel = props.event?.participantGroupLabel ?? 'Stamm'
|
||||||
|
|
||||||
const errors = reactive({
|
const errors = reactive({
|
||||||
vorname: '',
|
vorname: '',
|
||||||
nachname: '',
|
nachname: '',
|
||||||
@@ -46,7 +49,7 @@ const next = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (props.formData.localGroup === '-1') {
|
if (props.formData.localGroup === '-1') {
|
||||||
errors.localGroup = 'Bitte den Stamm auswählen.'
|
errors.localGroup = 'Bitte bei „' + localGroupLabel + '" eine Auswahl treffen.'
|
||||||
hasError = true
|
hasError = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +93,7 @@ const next = () => {
|
|||||||
<tr><td>Nachname:</td><td><input type="text" v-model="props.formData.nachname" /><ErrorText :message="errors.nachname"/></td></tr>
|
<tr><td>Nachname:</td><td><input type="text" v-model="props.formData.nachname" /><ErrorText :message="errors.nachname"/></td></tr>
|
||||||
<tr><td>Pfadiname:</td><td><input type="text" v-model="props.formData.pfadiname" /></td></tr>
|
<tr><td>Pfadiname:</td><td><input type="text" v-model="props.formData.pfadiname" /></td></tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Stamm:</td>
|
<td>{{ localGroupLabel }}:</td>
|
||||||
<td>
|
<td>
|
||||||
<select v-model="props.formData.localGroup">
|
<select v-model="props.formData.localGroup">
|
||||||
<option value="-1">Bitte wählen</option>
|
<option value="-1">Bitte wählen</option>
|
||||||
|
|||||||
@@ -26,6 +26,18 @@ class SaveInvoiceController extends CommonController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sicherheitsnetz hinter der Oberfläche: Wer gereist ist, steht im Zahlungsgrund auf Beleg und EüR und
|
||||||
|
// ist bei Fahrtkosten Pflicht. Geprüft wird hier und nicht im Command, weil auch Abrechnungskorrekturen
|
||||||
|
// durch den Command laufen -- sie bringen ihren Zahlungsgrund mit, aber keine Personen. Und vor dem
|
||||||
|
// Upload, damit eine abgelehnte Einreichung keinen verwaisten Beleg hinterlässt.
|
||||||
|
if ($invoiceType === InvoiceType::INVOICE_TYPE_TRAVELLING
|
||||||
|
&& Validator::make($request->all(), ['travellers' => 'required|string'])->fails()) {
|
||||||
|
return response()->json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Bitte gib an, welche Personen gereist sind.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
$uploadedFile = null;
|
$uploadedFile = null;
|
||||||
if (null !== $request->file('receipt')) {
|
if (null !== $request->file('receipt')) {
|
||||||
$maxFileSize = env('MAX_INVOICE_FILE_SIZE', 16);
|
$maxFileSize = env('MAX_INVOICE_FILE_SIZE', 16);
|
||||||
|
|||||||
@@ -41,10 +41,11 @@ const travelReasonComplete = computed(() =>
|
|||||||
/**
|
/**
|
||||||
* Wer gereist ist, steht später als Zahlungsgrund in der Belegliste, auf der EüR und auf dem Beleg. Wer
|
* Wer gereist ist, steht später als Zahlungsgrund in der Belegliste, auf der EüR und auf dem Beleg. Wer
|
||||||
* den Beleg einreicht, war meist selbst dabei -- deshalb der eigene Name als Vorschlag. Ohne Login
|
* den Beleg einreicht, war meist selbst dabei -- deshalb der eigene Name als Vorschlag. Ohne Login
|
||||||
* bleibt das Feld leer, und es hält den Ablauf auch dann nicht auf: Ein Name ist eine Hilfe, keine
|
* bleibt das Feld leer; weiter geht es erst, wenn jemand eingetragen ist -- wie beim Reisegrund.
|
||||||
* Bedingung.
|
|
||||||
*/
|
*/
|
||||||
const travellers = ref(data.userName || '');
|
const travellers = ref(data.userName || '');
|
||||||
|
|
||||||
|
const travellersComplete = computed(() => travellers.value.trim() !== '');
|
||||||
const have_receipt = ref('')
|
const have_receipt = ref('')
|
||||||
const amount = ref(0.00);
|
const amount = ref(0.00);
|
||||||
const invoiceType = ref(null);
|
const invoiceType = ref(null);
|
||||||
@@ -121,7 +122,7 @@ function handleFileChange(event) {
|
|||||||
/>
|
/>
|
||||||
</fieldset><br /><br />
|
</fieldset><br /><br />
|
||||||
|
|
||||||
<fieldset v-if="travelReasonComplete">
|
<fieldset v-if="travelReasonComplete && travellersComplete">
|
||||||
<legend><span style="font-weight: bolder;">Bist du mit dem ÖPNV gefahren oder besitzt du einen Beleg</span></legend>
|
<legend><span style="font-weight: bolder;">Bist du mit dem ÖPNV gefahren oder besitzt du einen Beleg</span></legend>
|
||||||
<input type="button" style="border-radius: 0; width: 100px;" @click="have_receipt='yes'" value="Ja" />
|
<input type="button" style="border-radius: 0; width: 100px;" @click="have_receipt='yes'" value="Ja" />
|
||||||
<input type="button" style="border-radius: 0; width: 100px;" @click="getDistanceAllowance" value="Nein" />
|
<input type="button" style="border-radius: 0; width: 100px;" @click="getDistanceAllowance" value="Nein" />
|
||||||
@@ -129,7 +130,8 @@ function handleFileChange(event) {
|
|||||||
|
|
||||||
<br /><br />
|
<br /><br />
|
||||||
|
|
||||||
<fieldset v-if="have_receipt === 'yes'">
|
<!-- Auch nach "Ja"/"Nein" gilt die Pflicht: Wer die Personen wieder löscht, kann nicht einreichen. -->
|
||||||
|
<fieldset v-if="travellersComplete && have_receipt === 'yes'">
|
||||||
<legend><span style="font-weight: bolder;">Wie hoch ist der Betrag?</span></legend>
|
<legend><span style="font-weight: bolder;">Wie hoch ist der Betrag?</span></legend>
|
||||||
<AmountInput v-model="amount" class="width-small" id="amount" name="amount" /> Euro
|
<AmountInput v-model="amount" class="width-small" id="amount" name="amount" /> Euro
|
||||||
|
|
||||||
@@ -161,7 +163,7 @@ function handleFileChange(event) {
|
|||||||
/>
|
/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset v-else-if="distanceAllowance != null">
|
<fieldset v-else-if="travellersComplete && distanceAllowance != null">
|
||||||
<legend><span style="font-weight: bolder;">Reiseinformationen</span></legend>
|
<legend><span style="font-weight: bolder;">Reiseinformationen</span></legend>
|
||||||
Gesamtlänge des Reisewegs:
|
Gesamtlänge des Reisewegs:
|
||||||
<NumericInput
|
<NumericInput
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enumerations;
|
||||||
|
|
||||||
|
use App\Models\GlobalSetting;
|
||||||
|
use App\Repositories\GlobalSettingRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wonach eine Veranstaltung bei der Anmeldung fragt: nach der Local Group (Mandant, im UI z. B. „Stamm") oder nach
|
||||||
|
* der Parent Group (übergeordnete Gruppe, im UI z. B. „Bund"). Die Bezeichnungen kommen aus den globalen
|
||||||
|
* Einstellungen.
|
||||||
|
*/
|
||||||
|
enum ParticipantGroupType: string
|
||||||
|
{
|
||||||
|
case LocalGroup = 'local_group';
|
||||||
|
case ParentGroup = 'parent_group';
|
||||||
|
|
||||||
|
public function label(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::LocalGroup => self::setting(GlobalSetting::LOCAL_GROUP_LABEL, 'Stamm'),
|
||||||
|
self::ParentGroup => self::setting(GlobalSetting::PARENT_GROUP_LABEL, 'Bund'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function pluralLabel(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::LocalGroup => self::setting(GlobalSetting::LOCAL_GROUP_LABEL_PLURAL, 'Stämme'),
|
||||||
|
self::ParentGroup => self::setting(GlobalSetting::PARENT_GROUP_LABEL_PLURAL, 'Bünde'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optionen für das Frontend: [['value' => ..., 'label' => ...], ...].
|
||||||
|
*
|
||||||
|
* @return array<int, array{value: string, label: string}>
|
||||||
|
*/
|
||||||
|
public static function options(): array
|
||||||
|
{
|
||||||
|
return array_map(
|
||||||
|
static fn (self $type): array => [
|
||||||
|
'value' => $type->value,
|
||||||
|
'label' => $type->label(),
|
||||||
|
],
|
||||||
|
self::cases(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Typ zu einem gespeicherten Wert; Unbekanntes fällt auf die Local Group zurück. */
|
||||||
|
public static function fromStored(?string $value): self
|
||||||
|
{
|
||||||
|
return self::tryFrom((string) $value) ?? self::LocalGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alle Bezeichnungen fürs Frontend (Shared Prop).
|
||||||
|
*
|
||||||
|
* @return array{localGroup: string, localGroupPlural: string, parentGroup: string, parentGroupPlural: string}
|
||||||
|
*/
|
||||||
|
public static function labels(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'localGroup' => self::LocalGroup->label(),
|
||||||
|
'localGroupPlural' => self::LocalGroup->pluralLabel(),
|
||||||
|
'parentGroup' => self::ParentGroup->label(),
|
||||||
|
'parentGroupPlural' => self::ParentGroup->pluralLabel(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function setting(string $key, string $default): string
|
||||||
|
{
|
||||||
|
return app(GlobalSettingRepository::class)->get($key, $default);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -66,10 +66,31 @@ abstract class AbstractEventPaymentModule implements EventPaymentModule
|
|||||||
return $this->sanitizeAgainst($this->getOptions(), $config);
|
return $this->sanitizeAgainst($this->getOptions(), $config);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param array<string, mixed> $config */
|
/**
|
||||||
|
* Vollständig heißt: alle Pflichtfelder befüllt **und** keine ungültigen Werte. Erst dann darf die
|
||||||
|
* Zahlungsart aktiv werden bzw. für Anmeldungen genutzt werden.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $config
|
||||||
|
*/
|
||||||
public function isConfigurationComplete(array $config): bool
|
public function isConfigurationComplete(array $config): bool
|
||||||
{
|
{
|
||||||
return $this->allRequiredFilled($this->getOptions(), $config);
|
return $this->allRequiredFilled($this->getOptions(), $config)
|
||||||
|
&& $this->configurationErrors($config) === [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inhaltliche Prüfung der Admin-Config über das reine „befüllt" hinaus (z.B. Prüfziffer einer
|
||||||
|
* IBAN). Leere Felder meldet hier niemand -- das ist Sache der Pflichtfeld-Prüfung.
|
||||||
|
*
|
||||||
|
* Standard: keine Prüfung. Bestandsmodule bleiben so, wie sie sind; eine Zahlungsart, bei der ein
|
||||||
|
* ungültiger Wert erst die Bank zurückweist (Lastschrift), prüft selbst.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $config
|
||||||
|
* @return array<string, string> Optionsname => Meldung
|
||||||
|
*/
|
||||||
|
public function configurationErrors(array $config): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ Rechnung) liegt gekapselt in einem Modul pro Zahlungsart. Aufgelöst wird über
|
|||||||
- `AbstractEventPaymentModule` — Basisklasse (Template-Method). Liefert die aus `getOptions()` abgeleiteten Helfer
|
- `AbstractEventPaymentModule` — Basisklasse (Template-Method). Liefert die aus `getOptions()` abgeleiteten Helfer
|
||||||
(`requiredOptionKeys()`, `sanitizeConfiguration()`, `isConfigurationComplete()`) und sinnvolle Default-/Stub-Bodies.
|
(`requiredOptionKeys()`, `sanitizeConfiguration()`, `isConfigurationComplete()`) und sinnvolle Default-/Stub-Bodies.
|
||||||
- `Modules/` — konkrete Module (flach, eine Klasse je Zahlungsart):
|
- `Modules/` — konkrete Module (flach, eine Klasse je Zahlungsart):
|
||||||
`AccountTransferPaymentModule` (Überweisung), `UndefinedPaymentModule` (Barzahlung/Sonstiges).
|
`AccountTransferPaymentModule` (Überweisung), `UndefinedPaymentModule` (Barzahlung/Sonstiges),
|
||||||
|
`SepaDirectDebitPaymentModule` (SEPA-Lastschrift, im Aufbau — siehe unten).
|
||||||
- `DTO/` — geteilte Request/Response-DTOs je Operation (`DoPayment*`, `CreateInvoice*`, `RegistrationSummary*`,
|
- `DTO/` — geteilte Request/Response-DTOs je Operation (`DoPayment*`, `CreateInvoice*`, `RegistrationSummary*`,
|
||||||
`GetRefundData*`, `TransactionMatch`).
|
`GetRefundData*`, `TransactionMatch`).
|
||||||
- `EventPaymentModuleRegistry` — statische Map `slug → Modul-Instanz` (`forSlug()`, `all()`, `slugs()`). **Neue Module
|
- `EventPaymentModuleRegistry` — statische Map `slug → Modul-Instanz` (`forSlug()`, `all()`, `slugs()`). **Neue Module
|
||||||
@@ -44,8 +45,23 @@ Rechnung) liegt gekapselt in einem Modul pro Zahlungsart. Aufgelöst wird über
|
|||||||
stehen, sonst verwirft `sanitizeParticipantOptions()` sie als unbekannte Schlüssel.
|
stehen, sonst verwirft `sanitizeParticipantOptions()` sie als unbekannte Schlüssel.
|
||||||
- `defaultConfiguration()` je Modul liefert die Start-Config beim Anlegen der Tenant-Instanz (`CreateTenantAction`),
|
- `defaultConfiguration()` je Modul liefert die Start-Config beim Anlegen der Tenant-Instanz (`CreateTenantAction`),
|
||||||
aktuell das Default-Symbol (`icon`): Überweisung `building-columns`, Sonstiges `coins`.
|
aktuell das Default-Symbol (`icon`): Überweisung `building-columns`, Sonstiges `coins`.
|
||||||
- **Aktivierungs-Guard:** Eine Tenant-Zahlungsmethode darf nur `active` werden, wenn alle `required`-Optionen befüllt
|
- **Vollständig = befüllt + gültig:** `isConfigurationComplete()` verlangt alle `required`-Optionen **und** ein leeres
|
||||||
sind (`isConfigurationComplete()`), erzwungen in `UpdateAvailablePaymentMethodAction`.
|
`configurationErrors(array $config): array<option, Meldung>`. Der Hook liefert in der Basis `[]`; ein Modul, dessen
|
||||||
|
Werte sonst erst die Bank zurückweist, prüft selbst (Lastschrift: IBAN, Gläubiger-ID, Vorlauf). Leere Felder meldet
|
||||||
|
der Hook nicht — das ist Sache der Pflichtfeld-Prüfung. Fassade: `PaymentMethod::configurationErrors()`.
|
||||||
|
- **Drei Guards, ein Maßstab (`isConfigurationComplete()`):**
|
||||||
|
- Tenant: `active` nur bei vollständiger Config (`UpdateAvailablePaymentMethodAction`, meldet die konkreten
|
||||||
|
Fehler; inaktiv speichern geht auch unfertig, als Entwurf). `CreateTenantAction` legt neue Stämme nur mit den
|
||||||
|
Zahlungsarten aktiv an, die schon vollständig sind — praktisch alle inaktiv, bis Bankdaten gepflegt sind.
|
||||||
|
- Event: `SetPaymentMethodsCommand` lehnt ab, wenn eine **zu schreibende** Event-Config (neuer Snapshot oder
|
||||||
|
Override) unvollständig ist — geprüft wird vor dem Schreiben, nichts wird halb übernommen.
|
||||||
|
- Anmeldung: `SignUpCommand` nimmt nur eine Zahlungsart an, die dem Event zugewiesen, beim Tenant aktiv und am
|
||||||
|
Event vollständig ist (`PaymentMethodRepository::isUsableForSignUp()`). Gilt auch für die Kurzanmeldung.
|
||||||
|
**Tests**, die eine Anmeldung durchspielen, müssen die Zahlungsart deshalb zuweisen — Trait
|
||||||
|
`Tests\Concerns\OffersAccountTransfer`.
|
||||||
|
- **Bankdaten-Sync:** `UpdateTenantPaymentAction` schreibt Kontoinhaber/IBAN/BIC des Stammes in die Tenant-Config von
|
||||||
|
Überweisung **und** Lastschrift (`BANK_ACCOUNT_SLUGS`); übrige Optionen bleiben stehen.
|
||||||
|
- Options-Typ `'number'` rendern beide Admin-Stellen als `<input type="number">`.
|
||||||
- **Config-Speicherung (JSON):** pro Tenant auf `available_payment_methods.configuration`, pro Event auf dem Pivot
|
- **Config-Speicherung (JSON):** pro Tenant auf `available_payment_methods.configuration`, pro Event auf dem Pivot
|
||||||
`event_payment_methods.configuration`. Beim Zuweisen an ein Event wird die Tenant-Config als **Snapshot kopiert**
|
`event_payment_methods.configuration`. Beim Zuweisen an ein Event wird die Tenant-Config als **Snapshot kopiert**
|
||||||
(Copy-on-Assign, spätere Tenant-Änderungen wirken NICHT nach). Sync erfolgt differenziell in
|
(Copy-on-Assign, spätere Tenant-Änderungen wirken NICHT nach). Sync erfolgt differenziell in
|
||||||
@@ -138,6 +154,27 @@ Beispiel GiroCode (nur Überweisung):
|
|||||||
- Parser (`App\Providers\BankStatementParseProvider`), `BankStatementRuleset` und `BankTransaction` liegen außerhalb
|
- Parser (`App\Providers\BankStatementParseProvider`), `BankStatementRuleset` und `BankTransaction` liegen außerhalb
|
||||||
dieser Schicht — sie sind zahlartneutral.
|
dieser Schicht — sie sind zahlartneutral.
|
||||||
|
|
||||||
|
## SEPA-Lastschrift (`SepaDirectDebitPaymentModule`, Slug `PAYMENT_SEPA_DIRECT_DEBIT`)
|
||||||
|
|
||||||
|
Keine Bankschnittstelle: mareike erzeugt eine Datei **pain.008.001.08** (ISO 20022, SEPA-Basislastschrift CORE, DK
|
||||||
|
DFÜ-Abkommen Anlage 3), die im Online-Banking hochgeladen wird. Bis dahin bleibt der Beitrag offen; danach gilt er als
|
||||||
|
ausgeglichen — unabhängig von Rücklastschriften.
|
||||||
|
|
||||||
|
- **Admin-Config:** `account_owner`/`iban`/`bic` (aus den Bankdaten des Stammes synchronisiert), `creditor_id`
|
||||||
|
(Gläubiger-ID, geprüft über `App\Support\CreditorId` — Mod 97-10 ohne Geschäftsbereichskennung; Testwert
|
||||||
|
`DE98ZZZ09999999999`), `pre_notification_days` (2–14, Default 5), `icon` (`file-signature`).
|
||||||
|
- **Bestandsdaten:** Migration `2026_10_04_140010` legt die Zahlungsart je Stamm **inaktiv** mit vorbefüllten
|
||||||
|
Bankdaten an (nur wenn schon Stämme existieren — bei Neuinstallation übernimmt der Seeder).
|
||||||
|
- **Abgestimmte Prozessentscheidungen** (für die nächsten Phasen):
|
||||||
|
- Mandat online per Checkbox im Anmeldeformular; Mandatsreferenz + Datum werden gespeichert. Nur IBANs aus EU/EWR,
|
||||||
|
dann sind ab 15.11.2026 keine (strukturierten) Adressen nötig.
|
||||||
|
- Anmeldemail zeigt einen Zeitraum: frühestens Anmeldetag + N, spätestens `registration_final_end` + N.
|
||||||
|
- Button „SEPA-Lastschriftdatei erzeugen" in der Event-Übersicht (`Overview.vue`): Einzugsdatum = heute + N
|
||||||
|
(nächster TARGET-Tag), Mail an jede*n Zahler*in mit genauem Datum, Betrag, Mandatsreferenz, Gläubiger-ID und
|
||||||
|
„Achte auf entsprechende Deckung"; Beitrag wird ausgeglichen. Datei bleibt gespeichert und erneut herunterladbar.
|
||||||
|
- **Offen:** Phase 2 (Teilnehmer-Optionen/Mandat, Pflicht-Checkbox muss `true` sein — heute gilt `false` als befüllt,
|
||||||
|
`registrationSummary()`, `getRefundData()` → `Known`), Phase 3 (Datei, Lauf-Protokoll, Mails).
|
||||||
|
|
||||||
## Anmelde-Zusammenfassung / Mail-Anzeige
|
## Anmelde-Zusammenfassung / Mail-Anzeige
|
||||||
|
|
||||||
- `registrationSummary(RegistrationSummaryRequest): RegistrationSummaryResponse` liefert den zahlungsspezifischen
|
- `registrationSummary(RegistrationSummaryRequest): RegistrationSummaryResponse` liefert den zahlungsspezifischen
|
||||||
@@ -180,7 +217,8 @@ Live-Inbetriebnahme einmal gegen die Produktionsdatenbank ausführen — ersetzt
|
|||||||
|
|
||||||
`tests/Unit/PaymentMethodOptionsTest`, `tests/Unit/EventPaymentModuleRegistryTest`,
|
`tests/Unit/PaymentMethodOptionsTest`, `tests/Unit/EventPaymentModuleRegistryTest`,
|
||||||
`tests/Unit/RegistrationSummaryTest`, `tests/Unit/BankStatementParseTest`, `tests/Unit/BankStatementMatchTest`,
|
`tests/Unit/RegistrationSummaryTest`, `tests/Unit/BankStatementParseTest`, `tests/Unit/BankStatementMatchTest`,
|
||||||
`tests/Unit/BankStatementRulesetTest`, `tests/Unit/RefundDataTest`, `tests/Feature/PaymentMethodConfigurationTest`,
|
`tests/Unit/BankStatementRulesetTest`, `tests/Unit/RefundDataTest`, `tests/Unit/CreditorIdTest`,
|
||||||
|
`tests/Feature/PaymentMethodConfigurationTest`, `tests/Feature/SignUpPaymentOptionsTest`,
|
||||||
`tests/Feature/EventParticipantPaymentSummaryTest`, `tests/Feature/BankStatementImportTest`,
|
`tests/Feature/EventParticipantPaymentSummaryTest`, `tests/Feature/BankStatementImportTest`,
|
||||||
`tests/Feature/RefundKnownAccountTest`, `tests/Feature/RefundCashPayerTest`.
|
`tests/Feature/RefundKnownAccountTest`, `tests/Feature/RefundCashPayerTest`.
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\EventPaymentModules;
|
namespace App\EventPaymentModules;
|
||||||
|
|
||||||
use App\EventPaymentModules\Modules\AccountTransferPaymentModule;
|
use App\EventPaymentModules\Modules\AccountTransferPaymentModule;
|
||||||
|
use App\EventPaymentModules\Modules\SepaDirectDebitPaymentModule;
|
||||||
use App\EventPaymentModules\Modules\UndefinedPaymentModule;
|
use App\EventPaymentModules\Modules\UndefinedPaymentModule;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -19,6 +20,7 @@ class EventPaymentModuleRegistry
|
|||||||
private const MODULES = [
|
private const MODULES = [
|
||||||
AccountTransferPaymentModule::class,
|
AccountTransferPaymentModule::class,
|
||||||
UndefinedPaymentModule::class,
|
UndefinedPaymentModule::class,
|
||||||
|
SepaDirectDebitPaymentModule::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
/** @var array<string, EventPaymentModule>|null Lazy gecachte Instanzen (slug => Modul). */
|
/** @var array<string, EventPaymentModule>|null Lazy gecachte Instanzen (slug => Modul). */
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\EventPaymentModules\Modules;
|
||||||
|
|
||||||
|
use App\EventPaymentModules\AbstractEventPaymentModule;
|
||||||
|
use App\EventPaymentModules\DTO\CreateInvoiceRequest;
|
||||||
|
use App\Models\PaymentMethod;
|
||||||
|
use App\Support\CreditorId;
|
||||||
|
use App\Support\Iban;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SEPA-Basislastschrift -- der Beitrag wird auf Grundlage eines Mandats vom Konto der zahlenden Person
|
||||||
|
* eingezogen.
|
||||||
|
*
|
||||||
|
* Eine Schnittstelle zur Bank gibt es nicht: mareike erzeugt eine Lastschriftdatei (pain.008), die im
|
||||||
|
* Online-Banking hochgeladen wird. Bis dahin bleibt der Beitrag offen.
|
||||||
|
*
|
||||||
|
* Das Gläubigerkonto kommt aus den Bankdaten des Stammes (wie bei der Überweisung, synchronisiert in
|
||||||
|
* {@see \App\Domains\Admin\Actions\UpdateTenantPayment\UpdateTenantPaymentAction}); dazu kommen die
|
||||||
|
* Gläubiger-ID und der Vorlauf bis zum Einzug.
|
||||||
|
*/
|
||||||
|
class SepaDirectDebitPaymentModule extends AbstractEventPaymentModule
|
||||||
|
{
|
||||||
|
public const string OPTION_CREDITOR_ID = 'creditor_id';
|
||||||
|
public const string OPTION_PRE_NOTIFICATION_DAYS = 'pre_notification_days';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Untergrenze des Vorlaufs: ein Bankarbeitstag, den die Bank für eine Basislastschrift braucht,
|
||||||
|
* plus der Tag, an dem die Datei hochgeladen wird.
|
||||||
|
*/
|
||||||
|
public const int MIN_PRE_NOTIFICATION_DAYS = 2;
|
||||||
|
|
||||||
|
/** Obergrenze: die gesetzliche Standardfrist der Vorabankündigung -- mehr verkürzt nichts mehr. */
|
||||||
|
public const int MAX_PRE_NOTIFICATION_DAYS = 14;
|
||||||
|
|
||||||
|
public static function slug(): string
|
||||||
|
{
|
||||||
|
return PaymentMethod::PAYMENT_SEPA_DIRECT_DEBIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function defaultName(): string
|
||||||
|
{
|
||||||
|
return 'SEPA-Lastschrift';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function defaultDescription(): ?string
|
||||||
|
{
|
||||||
|
return 'Der Beitrag wird auf Grundlage eines SEPA-Lastschriftmandats vom angegebenen Konto eingezogen.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function defaultConfiguration(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'icon' => 'file-signature',
|
||||||
|
self::OPTION_PRE_NOTIFICATION_DAYS => 5,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOptions(): array
|
||||||
|
{
|
||||||
|
$fromTenant = 'Wird aus den Bankdaten des Stammes übernommen.';
|
||||||
|
|
||||||
|
return [
|
||||||
|
['name' => 'account_owner', 'label' => 'Kontoinhaber', 'type' => 'string', 'required' => true, 'hint' => $fromTenant],
|
||||||
|
['name' => 'iban', 'label' => 'IBAN', 'type' => 'string', 'required' => true, 'hint' => $fromTenant],
|
||||||
|
['name' => 'bic', 'label' => 'BIC', 'type' => 'string', 'required' => false, 'hint' => $fromTenant],
|
||||||
|
[
|
||||||
|
'name' => self::OPTION_CREDITOR_ID,
|
||||||
|
'label' => 'Gläubiger-ID',
|
||||||
|
'type' => 'string',
|
||||||
|
'required' => true,
|
||||||
|
'hint' => 'Die Gläubiger-Identifikationsnummer wird kostenlos bei der Deutschen Bundesbank beantragt '
|
||||||
|
. '(glaeubiger-id.bundesbank.de), z. B. DE98ZZZ09999999999.',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => self::OPTION_PRE_NOTIFICATION_DAYS,
|
||||||
|
'label' => 'Vorlauf bis zum Einzug (Tage)',
|
||||||
|
'type' => 'number',
|
||||||
|
'required' => true,
|
||||||
|
'hint' => sprintf(
|
||||||
|
'So viele Tage nach dem Erzeugen der Lastschriftdatei wird eingezogen (%d bis %d). '
|
||||||
|
. 'Die Frist steht als verkürzte Vorabankündigung im Mandatstext.',
|
||||||
|
self::MIN_PRE_NOTIFICATION_DAYS,
|
||||||
|
self::MAX_PRE_NOTIFICATION_DAYS,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
['name' => 'icon', 'label' => 'Symbol', 'type' => 'icon', 'required' => false],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ein Fehler in diesen Werten fiele sonst erst der Bank auf -- beim Hochladen der Datei, wenn die
|
||||||
|
* Vorabankündigungen schon verschickt sind.
|
||||||
|
*/
|
||||||
|
public function configurationErrors(array $config): array
|
||||||
|
{
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
$iban = (string) ($config['iban'] ?? '');
|
||||||
|
if ($iban !== '' && !Iban::isValid($iban)) {
|
||||||
|
$errors['iban'] = 'Die IBAN ist ungültig.';
|
||||||
|
}
|
||||||
|
|
||||||
|
$creditorId = (string) ($config[self::OPTION_CREDITOR_ID] ?? '');
|
||||||
|
if ($creditorId !== '' && !CreditorId::isValid($creditorId)) {
|
||||||
|
$errors[self::OPTION_CREDITOR_ID] = 'Die Gläubiger-ID ist ungültig.';
|
||||||
|
}
|
||||||
|
|
||||||
|
$days = $config[self::OPTION_PRE_NOTIFICATION_DAYS] ?? '';
|
||||||
|
if ($days !== '' && $days !== null) {
|
||||||
|
$valid = filter_var($days, FILTER_VALIDATE_INT, ['options' => [
|
||||||
|
'min_range' => self::MIN_PRE_NOTIFICATION_DAYS,
|
||||||
|
'max_range' => self::MAX_PRE_NOTIFICATION_DAYS,
|
||||||
|
]]);
|
||||||
|
|
||||||
|
if ($valid === false) {
|
||||||
|
$errors[self::OPTION_PRE_NOTIFICATION_DAYS] = sprintf(
|
||||||
|
'Der Vorlauf muss eine ganze Zahl von %d bis %d Tagen sein.',
|
||||||
|
self::MIN_PRE_NOTIFICATION_DAYS,
|
||||||
|
self::MAX_PRE_NOTIFICATION_DAYS,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function invoiceClosingStatement(CreateInvoiceRequest $request): string
|
||||||
|
{
|
||||||
|
return sprintf('Der Betrag von %s wird per SEPA-Lastschrift eingezogen.', $request->amount->toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ use Illuminate\Database\Eloquent\Relations\HasOne;
|
|||||||
/**
|
/**
|
||||||
* @property string $name
|
* @property string $name
|
||||||
* @property string $type
|
* @property string $type
|
||||||
* @property string $billing_deadline
|
* @property string|null $billing_deadline Y-m-d, Abrechnungsschluss einschließlich des ganzen Tages
|
||||||
* @property string $distance_allowance
|
* @property string $distance_allowance
|
||||||
* @property boolean $mail_on_new
|
* @property boolean $mail_on_new
|
||||||
* @property boolean $allow_new
|
* @property boolean $allow_new
|
||||||
|
|||||||
+43
-2
@@ -5,6 +5,7 @@ namespace App\Models;
|
|||||||
|
|
||||||
use App\Casts\AmountCast;
|
use App\Casts\AmountCast;
|
||||||
use App\Enumerations\EatingHabit;
|
use App\Enumerations\EatingHabit;
|
||||||
|
use App\Enumerations\ParticipantGroupType;
|
||||||
use App\Enumerations\ParticipationFeeType;
|
use App\Enumerations\ParticipationFeeType;
|
||||||
use App\RelationModels\EventParticipationFee;
|
use App\RelationModels\EventParticipationFee;
|
||||||
use App\RelationModels\EventPaymentMethods;
|
use App\RelationModels\EventPaymentMethods;
|
||||||
@@ -13,6 +14,7 @@ use DateTime;
|
|||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property string $tenant
|
* @property string $tenant
|
||||||
@@ -49,7 +51,9 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
|||||||
* @property boolean $archived
|
* @property boolean $archived
|
||||||
* @property boolean $short_registration
|
* @property boolean $short_registration
|
||||||
* @property boolean $swimming_permission_required
|
* @property boolean $swimming_permission_required
|
||||||
|
* @property boolean $coc_check_required
|
||||||
* @property string|null $invoice_key
|
* @property string|null $invoice_key
|
||||||
|
* @property string $participant_group_type
|
||||||
*/
|
*/
|
||||||
class Event extends InstancedModel
|
class Event extends InstancedModel
|
||||||
{
|
{
|
||||||
@@ -99,8 +103,10 @@ class Event extends InstancedModel
|
|||||||
|
|
||||||
'short_registration',
|
'short_registration',
|
||||||
'swimming_permission_required',
|
'swimming_permission_required',
|
||||||
|
'coc_check_required',
|
||||||
|
|
||||||
'invoice_key',
|
'invoice_key',
|
||||||
|
'participant_group_type',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
@@ -133,6 +139,7 @@ class Event extends InstancedModel
|
|||||||
|
|
||||||
'short_registration' => 'boolean',
|
'short_registration' => 'boolean',
|
||||||
'swimming_permission_required' => 'boolean',
|
'swimming_permission_required' => 'boolean',
|
||||||
|
'coc_check_required' => 'boolean',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -186,6 +193,33 @@ class Event extends InstancedModel
|
|||||||
return $this->belongsToMany(Tenant::class, 'event_local_groups', 'event_id', 'local_group_id');
|
return $this->belongsToMany(Tenant::class, 'event_local_groups', 'event_id', 'local_group_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function participantGroupType() : ParticipantGroupType {
|
||||||
|
return ParticipantGroupType::fromStored($this->participant_group_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function asksForParentGroup() : bool {
|
||||||
|
return $this->participantGroupType() === ParticipantGroupType::ParentGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Was bei der Anmeldung zur Auswahl steht: die teilnehmenden Local Groups oder -- fragt die Veranstaltung nach der
|
||||||
|
* Parent Group -- deren Parent Groups, jede nur einmal (alphabetisch, „Ohne Zuordnung" zuletzt).
|
||||||
|
* Einzige Quelle für Anzeige und Prüfung der Auswahl.
|
||||||
|
*
|
||||||
|
* @return Collection<int, Tenant>|Collection<int, ParentGroup>
|
||||||
|
*/
|
||||||
|
public function participantGroupOptions() : Collection {
|
||||||
|
$localGroups = $this->localGroups()->with('parentGroup')->get();
|
||||||
|
|
||||||
|
if (!$this->asksForParentGroup()) {
|
||||||
|
return $localGroups;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ParentGroup::sortForDisplay(
|
||||||
|
$localGroups->pluck('parentGroup')->filter()->unique('id')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function eatingHabits() : BelongsToMany{
|
public function eatingHabits() : BelongsToMany{
|
||||||
return $this->belongsToMany(EatingHabit::class, 'event_allowed_eating_habits', 'event_id', 'eating_habit_id');
|
return $this->belongsToMany(EatingHabit::class, 'event_allowed_eating_habits', 'event_id', 'eating_habit_id');
|
||||||
}
|
}
|
||||||
@@ -234,13 +268,20 @@ class Event extends InstancedModel
|
|||||||
* jede Anmeldung dieser Gruppe zu. Null, wenn für die Veranstaltung noch keine Gebühr hinterlegt ist --
|
* jede Anmeldung dieser Gruppe zu. Null, wenn für die Veranstaltung noch keine Gebühr hinterlegt ist --
|
||||||
* dann ist eine Kurzanmeldung nicht möglich, weil `participation_type` am Teilnehmer Pflicht ist.
|
* dann ist eine Kurzanmeldung nicht möglich, weil `participation_type` am Teilnehmer Pflicht ist.
|
||||||
*/
|
*/
|
||||||
public function firstParticipationTypeSlug() : ?string {
|
/**
|
||||||
|
* Die erste hinterlegte Teilnahmegruppe -- die Kurzanmeldung meldet immer in dieser an.
|
||||||
|
*/
|
||||||
|
public function firstParticipationFee() : ?EventParticipationFee {
|
||||||
return collect([
|
return collect([
|
||||||
$this->participationFee1,
|
$this->participationFee1,
|
||||||
$this->participationFee2,
|
$this->participationFee2,
|
||||||
$this->participationFee3,
|
$this->participationFee3,
|
||||||
$this->participationFee4,
|
$this->participationFee4,
|
||||||
])->first(fn ($participationFee) => $participationFee !== null)?->type;
|
])->first(fn ($participationFee) => $participationFee !== null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function firstParticipationTypeSlug() : ?string {
|
||||||
|
return $this->firstParticipationFee()?->type;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -38,6 +38,7 @@ class EventParticipant extends InstancedModel
|
|||||||
'fee_type',
|
'fee_type',
|
||||||
'sibling_reduction',
|
'sibling_reduction',
|
||||||
'local_group',
|
'local_group',
|
||||||
|
'parent_group_id',
|
||||||
'birthday',
|
'birthday',
|
||||||
|
|
||||||
'address_1',
|
'address_1',
|
||||||
@@ -143,6 +144,22 @@ class EventParticipant extends InstancedModel
|
|||||||
return $this->belongsTo(Tenant::class, 'local_group', 'slug');
|
return $this->belongsTo(Tenant::class, 'local_group', 'slug');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Parent Group -- nur bei Veranstaltungen gesetzt, die danach statt nach der Local Group fragen. */
|
||||||
|
public function parentGroup()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(ParentGroup::class, 'parent_group_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Anzeigename der bei der Anmeldung gewählten Gruppe, je nachdem, wonach die Veranstaltung fragt. */
|
||||||
|
public function participantGroupName(Event $event) : string
|
||||||
|
{
|
||||||
|
if ($event->asksForParentGroup()) {
|
||||||
|
return $this->parentGroup?->name ?? 'Keine Angabe';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->localGroup?->name ?? 'Nicht im LV';
|
||||||
|
}
|
||||||
|
|
||||||
public function participationType()
|
public function participationType()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(ParticipationType::class, 'participation_type', 'slug');
|
return $this->belongsTo(ParticipationType::class, 'participation_type', 'slug');
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Scopes\CommonModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* App-weite Einstellung als Schlüssel/Wert-Paar (z. B. die Bezeichnungen „Stamm"/„Bund").
|
||||||
|
* Nicht mandantengebunden; gelesen wird über {@see \App\Repositories\GlobalSettingRepository}.
|
||||||
|
*
|
||||||
|
* @property string $key
|
||||||
|
* @property string $value
|
||||||
|
*/
|
||||||
|
class GlobalSetting extends CommonModel
|
||||||
|
{
|
||||||
|
public const string LOCAL_GROUP_LABEL = 'local_group_label';
|
||||||
|
public const string LOCAL_GROUP_LABEL_PLURAL = 'local_group_label_plural';
|
||||||
|
public const string PARENT_GROUP_LABEL = 'parent_group_label';
|
||||||
|
public const string PARENT_GROUP_LABEL_PLURAL = 'parent_group_label_plural';
|
||||||
|
|
||||||
|
protected $table = 'global_settings';
|
||||||
|
protected $primaryKey = 'key';
|
||||||
|
protected $keyType = 'string';
|
||||||
|
public $incrementing = false;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'key',
|
||||||
|
'value',
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Scopes\CommonModel;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Übergeordnete Gruppe (Parent Group, im UI z. B. „Bund"). Mehrere Mandanten (Local Groups) gehören zur selben
|
||||||
|
* Parent Group. Nicht mandantengebunden -- die Liste gilt app-weit.
|
||||||
|
*
|
||||||
|
* @property int $id
|
||||||
|
* @property string $name
|
||||||
|
* @property boolean $is_fallback
|
||||||
|
*/
|
||||||
|
class ParentGroup extends CommonModel
|
||||||
|
{
|
||||||
|
protected $table = 'parent_groups';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'is_fallback',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'is_fallback' => 'boolean',
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Die Gruppe „Ohne Zuordnung" -- Vorgabe für Mandanten, denen keine Gruppe zugewiesen wurde. */
|
||||||
|
public static function fallback() : self {
|
||||||
|
return self::where('is_fallback', true)->orderBy('id')->firstOrFail();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Anzeigereihenfolge: alphabetisch, „Ohne Zuordnung" (Fallback) immer zuletzt. */
|
||||||
|
public function scopeOrdered(Builder $query) : Builder {
|
||||||
|
return $query->orderBy('is_fallback')->orderBy('name');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dieselbe Reihenfolge wie {@see scopeOrdered()} für bereits geladene Gruppen.
|
||||||
|
*
|
||||||
|
* @param Collection<int, self> $parentGroups
|
||||||
|
* @return Collection<int, self>
|
||||||
|
*/
|
||||||
|
public static function sortForDisplay(Collection $parentGroups) : Collection {
|
||||||
|
return $parentGroups
|
||||||
|
->sort(static fn (self $a, self $b): int =>
|
||||||
|
[$a->is_fallback, mb_strtolower($a->name)] <=> [$b->is_fallback, mb_strtolower($b->name)])
|
||||||
|
->values();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tenants() : HasMany {
|
||||||
|
return $this->hasMany(Tenant::class, 'parent_group_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function eventParticipants() : HasMany {
|
||||||
|
return $this->hasMany(EventParticipant::class, 'parent_group_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ class PaymentMethod extends CommonModel
|
|||||||
|
|
||||||
public const string PAYMENT_ACCOUNT_TRANSACTION = 'PAYMENT_ACCOUNT_TRANSACTION';
|
public const string PAYMENT_ACCOUNT_TRANSACTION = 'PAYMENT_ACCOUNT_TRANSACTION';
|
||||||
public const string PAYMENT_NOT_DEFINED = 'PAYMENT_NOT_DEFINED';
|
public const string PAYMENT_NOT_DEFINED = 'PAYMENT_NOT_DEFINED';
|
||||||
|
public const string PAYMENT_SEPA_DIRECT_DEBIT = 'PAYMENT_SEPA_DIRECT_DEBIT';
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'slug',
|
'slug',
|
||||||
@@ -105,7 +106,7 @@ class PaymentMethod extends CommonModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prüft, ob alle Pflicht-Optionen eines Slugs in der Konfiguration befüllt (non-empty) sind.
|
* Prüft, ob alle Pflicht-Optionen eines Slugs befüllt (non-empty) und alle Werte gültig sind.
|
||||||
* Unbekannte Slugs (kein Modul) gelten als vollständig -- kein Modul, keine Pflichtfelder.
|
* Unbekannte Slugs (kein Modul) gelten als vollständig -- kein Modul, keine Pflichtfelder.
|
||||||
*
|
*
|
||||||
* @param array<string, mixed> $config
|
* @param array<string, mixed> $config
|
||||||
@@ -114,4 +115,15 @@ class PaymentMethod extends CommonModel
|
|||||||
{
|
{
|
||||||
return EventPaymentModuleRegistry::forSlug($slug)?->isConfigurationComplete($config) ?? true;
|
return EventPaymentModuleRegistry::forSlug($slug)?->isConfigurationComplete($config) ?? true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ungültige Werte einer Konfiguration (Optionsname => Meldung), etwa eine falsche Prüfziffer.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $config
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public static function configurationErrors(string $slug, array $config): array
|
||||||
|
{
|
||||||
|
return EventPaymentModuleRegistry::forSlug($slug)?->configurationErrors($config) ?? [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ namespace App\Models;
|
|||||||
|
|
||||||
use App\Scopes\CommonModel;
|
use App\Scopes\CommonModel;
|
||||||
use App\Support\Text;
|
use App\Support\Text;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property string $slug
|
* @property string $slug
|
||||||
@@ -35,6 +36,7 @@ use App\Support\Text;
|
|||||||
* @property string|null $tax_exemption_reason
|
* @property string|null $tax_exemption_reason
|
||||||
* @property string|null $tax_exemption_note
|
* @property string|null $tax_exemption_note
|
||||||
* @property string $vat_pricing_mode
|
* @property string $vat_pricing_mode
|
||||||
|
* @property int $parent_group_id
|
||||||
*/
|
*/
|
||||||
class Tenant extends CommonModel
|
class Tenant extends CommonModel
|
||||||
{
|
{
|
||||||
@@ -46,9 +48,15 @@ class Tenant extends CommonModel
|
|||||||
{
|
{
|
||||||
static::creating(static function (self $tenant): void {
|
static::creating(static function (self $tenant): void {
|
||||||
$tenant->invoice_prefix ??= strtoupper((string) $tenant->slug);
|
$tenant->invoice_prefix ??= strtoupper((string) $tenant->slug);
|
||||||
|
// Die Parent Group ist Pflicht; ohne Angabe landet der Mandant bei „Ohne Zuordnung".
|
||||||
|
$tenant->parent_group_id ??= ParentGroup::fallback()->id;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function parentGroup() : BelongsTo {
|
||||||
|
return $this->belongsTo(ParentGroup::class, 'parent_group_id');
|
||||||
|
}
|
||||||
|
|
||||||
public static function getTempDirectory() : string {
|
public static function getTempDirectory() : string {
|
||||||
return currentTenant()->slug . '/temp-data/';
|
return currentTenant()->slug . '/temp-data/';
|
||||||
}
|
}
|
||||||
@@ -95,6 +103,7 @@ class Tenant extends CommonModel
|
|||||||
'tax_number',
|
'tax_number',
|
||||||
'vat_id',
|
'vat_id',
|
||||||
'invoice_prefix',
|
'invoice_prefix',
|
||||||
|
'parent_group_id',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\Repositories\GlobalSettingRepository;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
@@ -12,7 +13,8 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
*/
|
*/
|
||||||
public function register(): void
|
public function register(): void
|
||||||
{
|
{
|
||||||
//
|
// Eine Instanz je Request, damit die Einstellungen nur einmal gelesen werden.
|
||||||
|
$this->app->scoped(GlobalSettingRepository::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace App\Providers;
|
|||||||
|
|
||||||
use App\Enumerations\EatingHabit;
|
use App\Enumerations\EatingHabit;
|
||||||
use App\Enumerations\InvoiceType;
|
use App\Enumerations\InvoiceType;
|
||||||
|
use App\Enumerations\ParticipantGroupType;
|
||||||
use App\Enumerations\RefundReason;
|
use App\Enumerations\RefundReason;
|
||||||
use App\Enumerations\RetentionReason;
|
use App\Enumerations\RetentionReason;
|
||||||
use App\Enumerations\TravelReason;
|
use App\Enumerations\TravelReason;
|
||||||
@@ -45,6 +46,8 @@ class GlobalDataProvider {
|
|||||||
'currentEvent' => $this->getCurrentEventData(),
|
'currentEvent' => $this->getCurrentEventData(),
|
||||||
'canAccessAdmin' => $canAccessAdmin,
|
'canAccessAdmin' => $canAccessAdmin,
|
||||||
'isMainAdmin' => $isMainAdmin,
|
'isMainAdmin' => $isMainAdmin,
|
||||||
|
// Bezeichnungen der Gruppen-Ebenen (z. B. Stamm/Stämme, Bund/Bünde) aus den globalen Einstellungen.
|
||||||
|
'groupLabels' => ParticipantGroupType::labels(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,6 +225,8 @@ class GlobalDataProvider {
|
|||||||
[
|
[
|
||||||
'localGroups' => Tenant::where(['is_active_local_group' => true])->get(),
|
'localGroups' => Tenant::where(['is_active_local_group' => true])->get(),
|
||||||
'eatingHabits' => EatingHabit::all(),
|
'eatingHabits' => EatingHabit::all(),
|
||||||
|
'participantGroupTypes' => ParticipantGroupType::options(),
|
||||||
|
'localGroupLabelPlural' => ParticipantGroupType::LocalGroup->pluralLabel(),
|
||||||
'paymentMethods' => AvailablePaymentMethod::where('active', true)->get()
|
'paymentMethods' => AvailablePaymentMethod::where('active', true)->get()
|
||||||
->map(fn (AvailablePaymentMethod $method) => new AvailablePaymentMethodResource($method)->toArray($request)),
|
->map(fn (AvailablePaymentMethod $method) => new AvailablePaymentMethodResource($method)->toArray($request)),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -21,6 +21,27 @@ class EventParticipationFee extends CommonModel
|
|||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/** Beitragsstufen in Anzeigereihenfolge; die Schlüssel sind die Spaltensuffixe von `amount_*`. */
|
||||||
|
public const array FEE_TYPE_LABELS = [
|
||||||
|
'standard' => 'Standardbeitrag',
|
||||||
|
'reduced' => 'Reduzierter Beitrag',
|
||||||
|
'solidarity' => 'Solidaritätsbeitrag',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die Beitragsstufen, die für diese Teilnahmegruppe tatsächlich hinterlegt sind. Den Standardbeitrag gibt es
|
||||||
|
* immer, reduzierter und Solidaritätsbeitrag sind optional. Nur für diese Stufen liefert
|
||||||
|
* `EventResource::calculateAmount()` einen Betrag.
|
||||||
|
*
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public function availableFeeTypes() : array {
|
||||||
|
return array_values(array_filter(
|
||||||
|
array_keys(self::FEE_TYPE_LABELS),
|
||||||
|
fn (string $feeType) => $this->{'amount_' . $feeType} !== null
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
public function tenant() : BelongsTo {
|
public function tenant() : BelongsTo {
|
||||||
return $this->belongsTo(Tenant::class, 'tenant', 'slug');
|
return $this->belongsTo(Tenant::class, 'tenant', 'slug');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -172,9 +172,13 @@ class CostUnitRepository {
|
|||||||
* damit immer dieselbe Gliederung, und eine fehlende Zeile lässt sich nicht mit einer vergessenen
|
* damit immer dieselbe Gliederung, und eine fehlende Zeile lässt sich nicht mit einer vergessenen
|
||||||
* verwechseln.
|
* verwechseln.
|
||||||
*
|
*
|
||||||
|
* Gespendete Auslagen zählen hier mit: Für die Einnahmen-Überschuss-Rechnung ist die Spende eine Ausgabe
|
||||||
|
* ihres Typs und zugleich eine Einnahme. Mit `$donationsOnly` liefert die Methode genau diesen Spendenanteil
|
||||||
|
* in derselben Gliederung -- die Gegenbuchungen lassen sich so ihrer Ausgabenkategorie zuordnen.
|
||||||
|
*
|
||||||
* @return array<string, array{type: InvoiceType, invoices: array<int, Invoice>, sum: Amount}>
|
* @return array<string, array{type: InvoiceType, invoices: array<int, Invoice>, sum: Amount}>
|
||||||
*/
|
*/
|
||||||
public function groupExpensesByType(CostUnit $costUnit) : array {
|
public function groupExpensesByType(CostUnit $costUnit, bool $donationsOnly = false) : array {
|
||||||
$groups = [];
|
$groups = [];
|
||||||
|
|
||||||
foreach (InvoiceType::countingAsExpense() as $invoiceType) {
|
foreach (InvoiceType::countingAsExpense() as $invoiceType) {
|
||||||
@@ -186,7 +190,11 @@ class CostUnitRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
foreach ($costUnit->invoices()->orderBy('invoice_number')->get() as $invoice) {
|
foreach ($costUnit->invoices()->orderBy('invoice_number')->get() as $invoice) {
|
||||||
if (!self::countsAsExpense($invoice) || !isset($groups[$invoice->type])) {
|
if (
|
||||||
|
!self::isBooked($invoice) ||
|
||||||
|
!isset($groups[$invoice->type]) ||
|
||||||
|
($donationsOnly && !$invoice->donation)
|
||||||
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,14 +209,22 @@ class CostUnitRepository {
|
|||||||
* Ob ein Beleg als Ausgabe zählt.
|
* Ob ein Beleg als Ausgabe zählt.
|
||||||
*
|
*
|
||||||
* Abgelehnt und gelöscht sind keine Ausgabe, weil kein Geld fließt. Dasselbe gilt für eine gespendete
|
* Abgelehnt und gelöscht sind keine Ausgabe, weil kein Geld fließt. Dasselbe gilt für eine gespendete
|
||||||
* Auslage: Auf die Auszahlung wurde verzichtet, die Kostenstelle gibt nichts aus.
|
* Auslage: Auf die Auszahlung wurde verzichtet, die Kostenstelle gibt nichts aus. Die Einnahmen-Überschuss-
|
||||||
|
* Rechnung sieht das anders und bucht die Spende als Ausgabe und Einnahme, siehe
|
||||||
|
* {@see self::groupExpensesByType()}.
|
||||||
*/
|
*/
|
||||||
private static function countsAsExpense(Invoice $invoice) : bool {
|
private static function countsAsExpense(Invoice $invoice) : bool {
|
||||||
|
return self::isBooked($invoice) && !$invoice->donation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ob ein Beleg überhaupt gebucht ist -- abgelehnt und gelöscht sind es nicht.
|
||||||
|
*/
|
||||||
|
private static function isBooked(Invoice $invoice) : bool {
|
||||||
return !in_array($invoice->status, [
|
return !in_array($invoice->status, [
|
||||||
InvoiceStatus::INVOICE_STATUS_DENIED,
|
InvoiceStatus::INVOICE_STATUS_DENIED,
|
||||||
InvoiceStatus::INVOICE_STATUS_DELETED,
|
InvoiceStatus::INVOICE_STATUS_DELETED,
|
||||||
], true)
|
], true);
|
||||||
&& !$invoice->donation;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function sumupEstimatedByInvoiceType(CostUnit $costUnit, InvoiceType $invoiceType) : Amount {
|
public function sumupEstimatedByInvoiceType(CostUnit $costUnit, InvoiceType $invoiceType) : Amount {
|
||||||
|
|||||||
@@ -73,12 +73,18 @@ class EventParticipantRepository {
|
|||||||
->first();
|
->first();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function groupByLocalGroup(Event $event, Request $request, ?string $filter = null) : array {
|
/** Ob es zur Veranstaltung schon Anmeldungen gibt -- abgemeldete eingeschlossen. */
|
||||||
|
public function existsForEvent(Event $event) : bool {
|
||||||
|
return EventParticipant::where('event_id', $event->id)->exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gruppiert nach der bei der Anmeldung gewählten Gruppe (Local oder Parent Group, je nach Veranstaltung). */
|
||||||
|
public function groupByParticipantGroup(Event $event, Request $request, ?string $filter = null) : array {
|
||||||
$allParticipants = $this->getForList($event, $request);
|
$allParticipants = $this->getForList($event, $request);
|
||||||
$participants = [];
|
$participants = [];
|
||||||
foreach ($allParticipants as $participant) {
|
foreach ($allParticipants as $participant) {
|
||||||
if ($filter === null || $participant['localgroup'] === $filter) {
|
if ($filter === null || $participant['participantGroup'] === $filter) {
|
||||||
$participants[$participant['localgroup']][] = $participant;
|
$participants[$participant['participantGroup']][] = $participant;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Repositories;
|
||||||
|
|
||||||
|
use App\Models\GlobalSetting;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zugriff auf die app-weiten Einstellungen. Im Container als `scoped` registriert -- über
|
||||||
|
* `app(GlobalSettingRepository::class)` beziehen, dann werden die Werte nur einmal pro Request gelesen
|
||||||
|
* (die Bezeichnungen werden je Teili-Zeile einer Liste gebraucht).
|
||||||
|
*/
|
||||||
|
class GlobalSettingRepository {
|
||||||
|
/** @var array<string, string>|null */
|
||||||
|
private ?array $values = null;
|
||||||
|
|
||||||
|
public function get(string $key, string $default) : string {
|
||||||
|
$value = $this->all()[$key] ?? null;
|
||||||
|
|
||||||
|
return ($value === null || trim($value) === '') ? $default : $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, string> $values */
|
||||||
|
public function setMany(array $values) : void {
|
||||||
|
foreach ($values as $key => $value) {
|
||||||
|
GlobalSetting::updateOrCreate(['key' => $key], ['value' => $value]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->values = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, string> */
|
||||||
|
private function all() : array {
|
||||||
|
return $this->values ??= GlobalSetting::pluck('value', 'key')->all();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ declare(strict_types=1);
|
|||||||
namespace App\Repositories;
|
namespace App\Repositories;
|
||||||
|
|
||||||
use App\Models\AvailablePaymentMethod;
|
use App\Models\AvailablePaymentMethod;
|
||||||
|
use App\Models\Event;
|
||||||
|
use App\Models\PaymentMethod;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zugriff auf die Zahlungsmethoden-Instanzen des Mandanten.
|
* Zugriff auf die Zahlungsmethoden-Instanzen des Mandanten.
|
||||||
@@ -24,4 +26,22 @@ class PaymentMethodRepository
|
|||||||
{
|
{
|
||||||
return (array) (AvailablePaymentMethod::where('slug', $slug)->first()?->configuration ?? []);
|
return (array) (AvailablePaymentMethod::where('slug', $slug)->first()?->configuration ?? []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Darf bei dieser Veranstaltung mit dieser Zahlungsart angemeldet werden?
|
||||||
|
*
|
||||||
|
* Nur wenn sie der Veranstaltung zugewiesen ist, beim Mandanten aktiv ist und ihre Config am Event
|
||||||
|
* vollständig und gültig ist. Sonst ließe sich über einen manipulierten Request eine Zahlungsart
|
||||||
|
* wählen, für die die Angaben zur Zahlung fehlen -- bei der Lastschrift etwa die Gläubiger-ID.
|
||||||
|
*/
|
||||||
|
public function isUsableForSignUp(Event $event, string $slug): bool
|
||||||
|
{
|
||||||
|
$method = $event->paymentMethods()->where('available_payment_methods.slug', $slug)->first();
|
||||||
|
|
||||||
|
if ($method === null || !$method->active) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return PaymentMethod::isConfigurationComplete($slug, (array) ($method->pivot->configuration ?? []));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loaded 100 of 143 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user