From f8dba18854f4b648159b54c1233b511bf7459966 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20G=C3=BCnther?=
Date: Wed, 5 Aug 2026 17:41:35 +0200
Subject: [PATCH] Icons for payment methods
---
.../Views/Partials/TenantPaymentMethods.vue | 6 +-
.../SetPaymentMethodsCommand.php | 82 +++++++++
.../SetPaymentMethodsRequest.php | 33 ++++
.../SetPaymentMethodsResponse.php | 9 +
.../UpdateEvent/UpdateEventCommand.php | 59 -------
.../UpdateEvent/UpdateEventRequest.php | 15 +-
.../Event/Controllers/DetailsController.php | 25 ++-
app/Domains/Event/Routes/api.php | 1 +
.../Event/Views/Partials/CommonSettings.vue | 75 --------
.../Views/Partials/ParticipationFees.vue | 136 ++++++++++++++-
.../SignUpForm/steps/StepPaymentMethod.vue | 13 +-
.../Modules/AccountTransferPaymentModule.php | 3 +-
.../Modules/UndefinedPaymentModule.php | 1 +
app/Views/Components/RichSelectBox.vue | 162 ++++++++++++++++++
resources/js/constants/paymentMethodIcons.js | 17 ++
.../PaymentMethodConfigurationTest.php | 26 +--
16 files changed, 473 insertions(+), 190 deletions(-)
create mode 100644 app/Domains/Event/Actions/SetPaymentMethods/SetPaymentMethodsCommand.php
create mode 100644 app/Domains/Event/Actions/SetPaymentMethods/SetPaymentMethodsRequest.php
create mode 100644 app/Domains/Event/Actions/SetPaymentMethods/SetPaymentMethodsResponse.php
create mode 100644 app/Views/Components/RichSelectBox.vue
create mode 100644 resources/js/constants/paymentMethodIcons.js
diff --git a/app/Domains/Admin/Views/Partials/TenantPaymentMethods.vue b/app/Domains/Admin/Views/Partials/TenantPaymentMethods.vue
index ac8d306..d960339 100644
--- a/app/Domains/Admin/Views/Partials/TenantPaymentMethods.vue
+++ b/app/Domains/Admin/Views/Partials/TenantPaymentMethods.vue
@@ -2,6 +2,8 @@
import {computed, ref} from 'vue';
import FullScreenModal from "../../../../Views/Components/FullScreenModal.vue";
import TextEditor from "../../../../Views/Components/TextEditor.vue";
+import RichSelectBox from "../../../../Views/Components/RichSelectBox.vue";
+import {PAYMENT_METHOD_ICONS} from "../../../../../resources/js/constants/paymentMethodIcons.js";
import {useAjax} from "../../../../../resources/js/components/ajaxHandler.js";
import {toast} from "vue3-toastify";
@@ -97,7 +99,9 @@ async function save() {
| {{ option.label }}* |
-
+
+
|
diff --git a/app/Domains/Event/Actions/SetPaymentMethods/SetPaymentMethodsCommand.php b/app/Domains/Event/Actions/SetPaymentMethods/SetPaymentMethodsCommand.php
new file mode 100644
index 0000000..874bae4
--- /dev/null
+++ b/app/Domains/Event/Actions/SetPaymentMethods/SetPaymentMethodsCommand.php
@@ -0,0 +1,82 @@
+request = $request;
+ }
+
+ public function execute(): SetPaymentMethodsResponse
+ {
+ $response = new SetPaymentMethodsResponse();
+
+ if (count($this->request->paymentMethods) === 0) {
+ $response->success = false;
+ $response->message = 'Mindestens eine Zahlungsmöglichkeit muss ausgewählt sein.';
+ return $response;
+ }
+
+ $this->syncPaymentMethods();
+
+ $this->request->event->save();
+ $response->success = true;
+
+ return $response;
+ }
+
+ /**
+ * Differenzieller Sync der Zahlungsmethoden mit Snapshot-Copy-Semantik:
+ * - entfernte Methoden werden gelöscht,
+ * - neue Methoden erhalten einen Snapshot der Tenant-Config (oder eines expliziten Overrides),
+ * - bestehende Methoden behalten ihre pro-Event-Config, sofern kein Override übergeben wird.
+ */
+ private function syncPaymentMethods(): void
+ {
+ $event = $this->request->event;
+ $desiredSlugs = $this->request->paymentMethods;
+ $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.
+ $tenantMethods = AvailablePaymentMethod::whereIn('slug', $desiredSlugs)->get()->keyBy('slug');
+
+ foreach ($desiredSlugs as $slug) {
+ $override = $overrides[$slug] ?? null;
+
+ if (isset($existing[$slug])) {
+ // Bestehende Zuweisung: Config nur bei explizitem Override anpassen, sonst unverändert lassen.
+ if ($override !== null) {
+ $row = $existing[$slug];
+ $row->configuration = PaymentMethod::sanitizeConfiguration($slug, $override);
+ $row->save();
+ }
+ continue;
+ }
+
+ // Neue Zuweisung: expliziter Override oder Snapshot der Tenant-Config.
+ $snapshot = $override ?? ($tenantMethods[$slug]->configuration ?? []);
+ EventPaymentMethods::create([
+ 'event_id' => $event->id,
+ 'slug' => $slug,
+ 'configuration' => PaymentMethod::sanitizeConfiguration($slug, $snapshot ?? []),
+ ]);
+ }
+ }
+}
diff --git a/app/Domains/Event/Actions/SetPaymentMethods/SetPaymentMethodsRequest.php b/app/Domains/Event/Actions/SetPaymentMethods/SetPaymentMethodsRequest.php
new file mode 100644
index 0000000..c705a62
--- /dev/null
+++ b/app/Domains/Event/Actions/SetPaymentMethods/SetPaymentMethodsRequest.php
@@ -0,0 +1,33 @@
+
+ */
+ public array $paymentMethods;
+
+ /**
+ * Optionale pro-Event-Config-Overrides je Zahlungsmethode: [slug => [option => value]].
+ * Fehlt ein Slug, bleibt die bestehende Config erhalten bzw. wird beim ersten Zuweisen
+ * aus der Tenant-Config als Snapshot kopiert.
+ *
+ * @var array>
+ */
+ public array $paymentMethodConfigurations;
+
+ public function __construct(Event $event, array $paymentMethods, array $paymentMethodConfigurations = [])
+ {
+ $this->event = $event;
+ $this->paymentMethods = $paymentMethods;
+ $this->paymentMethodConfigurations = $paymentMethodConfigurations;
+ }
+}
diff --git a/app/Domains/Event/Actions/SetPaymentMethods/SetPaymentMethodsResponse.php b/app/Domains/Event/Actions/SetPaymentMethods/SetPaymentMethodsResponse.php
new file mode 100644
index 0000000..3fd5df2
--- /dev/null
+++ b/app/Domains/Event/Actions/SetPaymentMethods/SetPaymentMethodsResponse.php
@@ -0,0 +1,9 @@
+request->paymentMethods) === 0) {
- $response->success = false;
- $response->message = 'Mindestens eine Zahlungsmöglichkeit muss ausgewählt sein.';
- return $response;
- }
-
$this->request->event->name = $this->request->eventName;
$this->request->event->location = $this->request->eventLocation;
$this->request->event->postal_code = $this->request->postalCode;
@@ -45,58 +35,9 @@ class UpdateEventCommand {
$this->request->event->localGroups()->attach($contributingLocalGroup);
}
- $this->syncPaymentMethods();
-
$this->request->event->save();
$response->success = true;
return $response;
}
-
- /**
- * Differenzieller Sync der Zahlungsmethoden mit Snapshot-Copy-Semantik:
- * - entfernte Methoden werden gelöscht,
- * - neue Methoden erhalten einen Snapshot der Tenant-Config (oder eines expliziten Overrides),
- * - bestehende Methoden behalten ihre pro-Event-Config, sofern kein Override übergeben wird.
- */
- private function syncPaymentMethods(): void
- {
- $event = $this->request->event;
- $desiredSlugs = $this->request->paymentMethods;
- $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.
- $tenantMethods = AvailablePaymentMethod::whereIn('slug', $desiredSlugs)->get()->keyBy('slug');
-
- foreach ($desiredSlugs as $slug) {
- $override = $overrides[$slug] ?? null;
-
- if (isset($existing[$slug])) {
- // Bestehende Zuweisung: Config nur bei explizitem Override anpassen, sonst unverändert lassen.
- if ($override !== null) {
- $row = $existing[$slug];
- $row->configuration = PaymentMethod::sanitizeConfiguration($slug, $override);
- $row->save();
- }
- continue;
- }
-
- // Neue Zuweisung: expliziter Override oder Snapshot der Tenant-Config.
- $snapshot = $override ?? ($tenantMethods[$slug]->configuration ?? []);
- EventPaymentMethods::create([
- 'event_id' => $event->id,
- 'slug' => $slug,
- 'configuration' => PaymentMethod::sanitizeConfiguration($slug, $snapshot ?? []),
- ]);
- }
- }
}
diff --git a/app/Domains/Event/Actions/UpdateEvent/UpdateEventRequest.php b/app/Domains/Event/Actions/UpdateEvent/UpdateEventRequest.php
index ffb9f41..da811b4 100644
--- a/app/Domains/Event/Actions/UpdateEvent/UpdateEventRequest.php
+++ b/app/Domains/Event/Actions/UpdateEvent/UpdateEventRequest.php
@@ -21,18 +21,9 @@ class UpdateEventRequest {
public Amount $supportPerPerson;
public array $contributingLocalGroups;
public array $eatingHabits;
- public array $paymentMethods;
- /**
- * Optionale pro-Event-Config-Overrides je Zahlungsmethode: [slug => [option => value]].
- * Fehlt ein Slug, bleibt die bestehende Config erhalten bzw. wird beim ersten Zuweisen
- * aus der Tenant-Config als Snapshot kopiert.
- *
- * @var array>
- */
- public array $paymentMethodConfigurations;
-
- 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, array $paymentMethods, array $paymentMethodConfigurations = []) {
+ public function __construct(Event $event, string $eventName, string $eventLocation, string $postalCode, string $email, DateTime $earlyBirdEnd, DateTime $registrationFinalEnd, int $alcoholicsAge, bool $sendWeeklyReports, bool $registrationAllowed, Amount $flatSupport, Amount $supportPerPerson, array $contributingLocalGroups, array $eatingHabits)
+ {
$this->event = $event;
$this->eventName = $eventName;
$this->eventLocation = $eventLocation;
@@ -47,7 +38,5 @@ class UpdateEventRequest {
$this->supportPerPerson = $supportPerPerson;
$this->contributingLocalGroups = $contributingLocalGroups;
$this->eatingHabits = $eatingHabits;
- $this->paymentMethods = $paymentMethods;
- $this->paymentMethodConfigurations = $paymentMethodConfigurations;
}
}
diff --git a/app/Domains/Event/Controllers/DetailsController.php b/app/Domains/Event/Controllers/DetailsController.php
index c96f840..b64c281 100644
--- a/app/Domains/Event/Controllers/DetailsController.php
+++ b/app/Domains/Event/Controllers/DetailsController.php
@@ -4,22 +4,21 @@ namespace App\Domains\Event\Controllers;
use App\Domains\Event\Actions\SetParticipationFees\SetParticipationFeesCommand;
use App\Domains\Event\Actions\SetParticipationFees\SetParticipationFeesRequest;
+use App\Domains\Event\Actions\SetPaymentMethods\SetPaymentMethodsCommand;
+use App\Domains\Event\Actions\SetPaymentMethods\SetPaymentMethodsRequest;
use App\Domains\Event\Actions\UpdateEvent\UpdateEventCommand;
use App\Domains\Event\Actions\UpdateEvent\UpdateEventRequest;
use App\Domains\Event\Actions\UpdateManagers\UpdateManagersCommand;
use App\Domains\Event\Actions\UpdateManagers\UpdateManagersRequest;
use App\Enumerations\ParticipationFeeType;
use App\Enumerations\ParticipationType;
-use App\Models\EventParticipant;
use App\Providers\InertiaProvider;
use App\Providers\PdfGenerateAndDownloadProvider;
-use App\Resources\EventResource;
use App\Scopes\CommonController;
use App\ValueObjects\Amount;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
-use function Symfony\Component\String\b;
class DetailsController extends CommonController {
public function __invoke(string $eventId) {
@@ -42,8 +41,6 @@ class DetailsController extends CommonController {
$contributinLocalGroups = $request->input('contributingLocalGroups');
$eatingHabits = $request->input('eatingHabits');
- $paymentMethods = $request->input('paymentMethods');
- $paymentMethodConfigurations = (array) $request->input('paymentMethodConfigurations', []);
$eventUpdateRequest = new UpdateEventRequest(
$event,
@@ -59,9 +56,7 @@ class DetailsController extends CommonController {
$flatSupport,
$supportPerPerson,
$contributinLocalGroups,
- $eatingHabits,
- $paymentMethods,
- $paymentMethodConfigurations
+ $eatingHabits
);
$eventUpdateCommand = new UpdateEventCommand($eventUpdateRequest);
@@ -70,6 +65,20 @@ class DetailsController extends CommonController {
return response()->json(['status' => $response->success ? 'success' : 'error', 'message' => $response->message]);
}
+ public function updatePaymentMethods(int $eventId, Request $request): JsonResponse
+ {
+ $event = $this->events->getById($eventId);
+
+ $paymentMethods = $request->input('paymentMethods', []);
+ $paymentMethodConfigurations = (array)$request->input('paymentMethodConfigurations', []);
+
+ $setPaymentMethodsRequest = new SetPaymentMethodsRequest($event, $paymentMethods, $paymentMethodConfigurations);
+ $setPaymentMethodsCommand = new SetPaymentMethodsCommand($setPaymentMethodsRequest);
+ $response = $setPaymentMethodsCommand->execute();
+
+ return response()->json(['status' => $response->success ? 'success' : 'error', 'message' => $response->message]);
+ }
+
public function updateEventManagers(int $eventId, Request $request) : JsonResponse {
$event = $this->events->getById($eventId);
diff --git a/app/Domains/Event/Routes/api.php b/app/Domains/Event/Routes/api.php
index a4d8c47..34cb9b6 100644
--- a/app/Domains/Event/Routes/api.php
+++ b/app/Domains/Event/Routes/api.php
@@ -45,6 +45,7 @@ Route::prefix('api/v1')
Route::post('/event-managers', [DetailsController::class, 'updateEventManagers']);
Route::post('/participation-fees', [DetailsController::class, 'updateParticipationFees']);
Route::post('/common-settings', [DetailsController::class, 'updateCommonSettings']);
+ Route::post('/payment-methods', [DetailsController::class, 'updatePaymentMethods']);
});
diff --git a/app/Domains/Event/Views/Partials/CommonSettings.vue b/app/Domains/Event/Views/Partials/CommonSettings.vue
index 78e7aec..bed9e87 100644
--- a/app/Domains/Event/Views/Partials/CommonSettings.vue
+++ b/app/Domains/Event/Views/Partials/CommonSettings.vue
@@ -2,7 +2,6 @@
import {onMounted, reactive, ref} from "vue";
import ErrorText from "../../../../Views/Components/ErrorText.vue";
import AmountInput from "../../../../Views/Components/AmountInput.vue";
-import TextEditor from "../../../../Views/Components/TextEditor.vue";
import {request} from "../../../../../resources/js/components/HttpClient.js";
import {toast} from "vue3-toastify";
@@ -17,14 +16,10 @@ const emit = defineEmits(['close'])
const dynmicProps = reactive({
localGroups: [],
eatingHabits:[],
- paymentMethods: []
});
const contributingLocalGroups = ref([])
const eatingHabits = ref([]);
- const paymentMethods = ref([]);
- // pro-Event-Config je Zahlungsmethode: { slug: { option: value } }
- const paymentMethodConfigurations = reactive({});
const errors = reactive({})
@@ -51,30 +46,9 @@ const emit = defineEmits(['close'])
contributingLocalGroups.value = props.event.contributingLocalGroups?.map(t => t.id) ?? []
eatingHabits.value = props.event.eatingHabits?.map(t => t.id) ?? []
- paymentMethods.value = props.event.paymentMethods?.map(t => t.slug) ?? []
-
- // Pro-Event-Config vorbelegen: bereits gespeicherte Pivot-Config des Events hat Vorrang,
- // sonst der Tenant-Standard (Snapshot-Quelle für neu zugewiesene Methoden).
- const eventConfigBySlug = {}
- for (const pm of props.event.paymentMethods ?? []) {
- eventConfigBySlug[pm.slug] = pm.configuration ?? {}
- }
- for (const method of dynmicProps.paymentMethods ?? []) {
- const source = eventConfigBySlug[method.slug] ?? method.configuration ?? {}
- const config = {}
- for (const option of method.optionsSchema ?? []) {
- config[option.name] = source[option.name] ?? ''
- }
- paymentMethodConfigurations[method.slug] = config
- }
});
async function save() {
- if (paymentMethods.value.length === 0) {
- toast.error('Mindestens eine Zahlungsmöglichkeit muss ausgewählt sein.')
- return
- }
-
const response = await request('/api/v1/event/details/' + props.event.id + '/common-settings', {
method: 'POST',
headers: {
@@ -94,8 +68,6 @@ const emit = defineEmits(['close'])
supportPerson: formData.supportPerson,
contributingLocalGroups: contributingLocalGroups.value,
eatingHabits: eatingHabits.value,
- paymentMethods: paymentMethods.value,
- paymentMethodConfigurations: paymentMethodConfigurations,
}
})
@@ -227,31 +199,6 @@ const emit = defineEmits(['close'])
-
-
- | Zahlungsmöglichkeiten |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- |
-
@@ -308,26 +255,4 @@ const emit = defineEmits(['close'])
width: 250px;
}
- .payment-method-config {
- margin: 6px 0 12px 24px;
- padding-left: 10px;
- border-left: 2px solid #d1d5db;
- }
-
- .payment-method-config-row {
- margin-bottom: 6px;
- }
-
- .payment-method-config-row label {
- display: block;
- font-size: 0.85rem;
- color: #374151;
- margin-bottom: 2px;
- }
-
- .required-marker {
- color: #ef4444;
- margin-left: 2px;
- }
-
diff --git a/app/Domains/Event/Views/Partials/ParticipationFees.vue b/app/Domains/Event/Views/Partials/ParticipationFees.vue
index 21cd001..4291ac1 100644
--- a/app/Domains/Event/Views/Partials/ParticipationFees.vue
+++ b/app/Domains/Event/Views/Partials/ParticipationFees.vue
@@ -1,16 +1,59 @@
+
+
+
+
+
+
+ -
+
+ {{ option.label }}
+
+
+
+
+
+
diff --git a/resources/js/constants/paymentMethodIcons.js b/resources/js/constants/paymentMethodIcons.js
new file mode 100644
index 0000000..0bffb79
--- /dev/null
+++ b/resources/js/constants/paymentMethodIcons.js
@@ -0,0 +1,17 @@
+// Kuratierte Liste zahlungsrelevanter Font-Awesome-Solid-Icons für die Symbol-Auswahl einer
+// Zahlungsart. Die Namen sind über app/Views/Components/Icon.vue (free-solid) gedeckt und werden
+// beim Build gebündelt (kein Netzwerk-Load). value === icon (FA-Solid-Name im kebab-case).
+export const PAYMENT_METHOD_ICONS = [
+ {value: 'building-columns', label: 'Bank / Überweisung', icon: 'building-columns'},
+ {value: 'money-bill-wave', label: 'Bargeld', icon: 'money-bill-wave'},
+ {value: 'credit-card', label: 'Kreditkarte', icon: 'credit-card'},
+ {value: 'wallet', label: 'Geldbörse', icon: 'wallet'},
+ {value: 'coins', label: 'Münzen', icon: 'coins'},
+ {value: 'euro-sign', label: 'Euro', icon: 'euro-sign'},
+ {value: 'hand-holding-dollar', label: 'Spende', icon: 'hand-holding-dollar'},
+ {value: 'sack-dollar', label: 'Geldsack', icon: 'sack-dollar'},
+ {value: 'money-check-dollar', label: 'Scheck', icon: 'money-check-dollar'},
+ {value: 'receipt', label: 'Beleg', icon: 'receipt'},
+ {value: 'piggy-bank', label: 'Sparschwein', icon: 'piggy-bank'},
+ {value: 'mobile-screen', label: 'Mobil / App', icon: 'mobile-screen'},
+]
diff --git a/tests/Feature/PaymentMethodConfigurationTest.php b/tests/Feature/PaymentMethodConfigurationTest.php
index c7127a4..e46f003 100644
--- a/tests/Feature/PaymentMethodConfigurationTest.php
+++ b/tests/Feature/PaymentMethodConfigurationTest.php
@@ -4,13 +4,12 @@ namespace Tests\Feature;
use App\Domains\Admin\Actions\UpdateAvailablePaymentMethod\UpdateAvailablePaymentMethodAction;
use App\Domains\Admin\Actions\UpdateAvailablePaymentMethod\UpdateAvailablePaymentMethodRequest;
-use App\Domains\Event\Actions\UpdateEvent\UpdateEventCommand;
-use App\Domains\Event\Actions\UpdateEvent\UpdateEventRequest;
+use App\Domains\Event\Actions\SetPaymentMethods\SetPaymentMethodsCommand;
+use App\Domains\Event\Actions\SetPaymentMethods\SetPaymentMethodsRequest;
use App\Models\AvailablePaymentMethod;
use App\Models\Event;
use App\Models\PaymentMethod;
use App\Models\Tenant;
-use App\ValueObjects\Amount;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
@@ -156,26 +155,9 @@ class PaymentMethodConfigurationTest extends TestCase
*/
private function runUpdateEvent(Event $event, array $slugs, array $configs = []): void
{
- $request = new UpdateEventRequest(
- $event,
- $event->name,
- $event->location,
- $event->postal_code,
- $event->email,
- new \DateTime('2026-07-01'),
- new \DateTime('2026-07-20'),
- 16,
- false,
- false,
- Amount::fromString('0'),
- Amount::fromString('0'),
- [],
- [],
- $slugs,
- $configs,
- );
+ $request = new SetPaymentMethodsRequest($event, $slugs, $configs);
- $response = new UpdateEventCommand($request)->execute();
+ $response = new SetPaymentMethodsCommand($request)->execute();
$this->assertTrue($response->success, $response->message);
}
}