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 @@ + + + + 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); } }