From a5e4e55df50f55678ca265129754698f768cdfe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCnrher?= Date: Tue, 29 Sep 2026 14:35:33 +0200 Subject: [PATCH] Support oft parent groups --- CLAUDE.md | 20 ++ .../CreateParentGroupAction.php | 36 ++ .../CreateParentGroupRequest.php | 11 + .../CreateParentGroupResponse.php | 12 + .../CreateTenant/CreateTenantAction.php | 10 +- .../CreateTenant/CreateTenantRequest.php | 4 + .../DeleteParentGroupAction.php | 38 +++ .../DeleteParentGroupRequest.php | 15 + .../DeleteParentGroupResponse.php | 9 + .../RenameParentGroupAction.php | 31 ++ .../RenameParentGroupRequest.php | 14 + .../RenameParentGroupResponse.php | 9 + .../UpdateGlobalSettingsAction.php | 38 +++ .../UpdateGlobalSettingsRequest.php | 17 + .../UpdateGlobalSettingsResponse.php | 9 + .../UpdateTenantGeneralAction.php | 9 + .../UpdateTenantGeneralRequest.php | 3 + .../GlobalSettingsGetController.php | 18 + .../GlobalSettingsPageController.php | 16 + .../GlobalSettingsUpdateController.php | 28 ++ .../ParentGroupCreateController.php | 24 ++ .../ParentGroupDeleteController.php | 31 ++ .../ParentGroupListApiController.php | 23 ++ .../ParentGroupListPageController.php | 19 ++ .../ParentGroupUpdateController.php | 31 ++ .../Controllers/TenantCreateController.php | 1 + .../TenantGeneralGetController.php | 7 + .../TenantGeneralUpdateController.php | 1 + .../Controllers/TenantListApiController.php | 10 +- .../Controllers/TenantListPageController.php | 5 +- .../AdminParentGroupRepository.php | 32 ++ app/Domains/Admin/Routes/api.php | 19 ++ app/Domains/Admin/Routes/web.php | 4 + app/Domains/Admin/Views/GlobalSettings.vue | 116 +++++++ app/Domains/Admin/Views/ParentGroupList.vue | 249 ++++++++++++++ .../Admin/Views/Partials/TenantGeneral.vue | 19 +- app/Domains/Admin/Views/TenantList.vue | 36 +- .../CreateEvent/CreateEventCommand.php | 1 + .../CreateEvent/CreateEventRequest.php | 6 +- .../ShortSignUp/ShortSignUpCommand.php | 28 +- .../Event/Actions/SignUp/SignUpCommand.php | 1 + .../Event/Actions/SignUp/SignUpRequest.php | 3 + .../UpdateEvent/UpdateEventCommand.php | 10 + .../UpdateEvent/UpdateEventRequest.php | 6 +- .../UpdateParticipantCommand.php | 28 +- .../UpdateParticipantRequest.php | 7 +- .../Event/Controllers/CreateController.php | 5 +- .../Event/Controllers/DetailsController.php | 13 +- .../MailCompose/ByGroupController.php | 2 +- .../ParticipantUpdateController.php | 2 +- .../Event/Controllers/SignupController.php | 15 +- app/Domains/Event/Views/Create.vue | 12 + app/Domains/Event/Views/Details.vue | 3 +- .../Event/Views/Partials/CommonSettings.vue | 21 +- .../Event/Views/Partials/ParticipantData.vue | 17 +- .../Event/Views/Partials/ParticipantsList.vue | 4 +- .../composables/useShortSignupForm.js | 10 +- .../ShortSignUpForm/steps/ShortStepPerson.vue | 7 +- .../steps/ShortStepSummary.vue | 6 +- .../Views/Partials/SignUpForm/SignupForm.vue | 2 +- .../SignUpForm/composables/useSignupForm.js | 5 +- .../SignUpForm/steps/StepPersonalData.vue | 9 +- app/Enumerations/ParticipantGroupType.php | 75 +++++ app/Models/Event.php | 31 ++ app/Models/EventParticipant.php | 17 + app/Models/GlobalSetting.php | 30 ++ app/Models/ParentGroup.php | 61 ++++ app/Models/Tenant.php | 9 + app/Providers/AppServiceProvider.php | 4 +- app/Providers/GlobalDataProvider.php | 5 + .../EventParticipantRepository.php | 12 +- app/Repositories/GlobalSettingRepository.php | 35 ++ app/Resources/EventParticipantResource.php | 7 +- app/Resources/EventResource.php | 14 + app/Scopes/CommonController.php | 6 + ...2026_09_29_140010_create_parent_groups.php | 102 ++++++ resources/js/layouts/AdminAppLayout.vue | 9 +- .../views/csvs/participant-list.blade.php | 6 +- resources/views/pdfs/amount-list.blade.php | 4 +- resources/views/pdfs/drinking-list.blade.php | 4 +- resources/views/pdfs/first-aid-list.blade.php | 4 +- resources/views/pdfs/kitchen-list.blade.php | 4 +- .../pdfs/photo-permission-list.blade.php | 4 +- tests/Feature/ParentGroupTest.php | 307 ++++++++++++++++++ tests/Feature/ShortSignUpTest.php | 67 +++- version | 2 +- 86 files changed, 1936 insertions(+), 80 deletions(-) create mode 100644 app/Domains/Admin/Actions/CreateParentGroup/CreateParentGroupAction.php create mode 100644 app/Domains/Admin/Actions/CreateParentGroup/CreateParentGroupRequest.php create mode 100644 app/Domains/Admin/Actions/CreateParentGroup/CreateParentGroupResponse.php create mode 100644 app/Domains/Admin/Actions/DeleteParentGroup/DeleteParentGroupAction.php create mode 100644 app/Domains/Admin/Actions/DeleteParentGroup/DeleteParentGroupRequest.php create mode 100644 app/Domains/Admin/Actions/DeleteParentGroup/DeleteParentGroupResponse.php create mode 100644 app/Domains/Admin/Actions/RenameParentGroup/RenameParentGroupAction.php create mode 100644 app/Domains/Admin/Actions/RenameParentGroup/RenameParentGroupRequest.php create mode 100644 app/Domains/Admin/Actions/RenameParentGroup/RenameParentGroupResponse.php create mode 100644 app/Domains/Admin/Actions/UpdateGlobalSettings/UpdateGlobalSettingsAction.php create mode 100644 app/Domains/Admin/Actions/UpdateGlobalSettings/UpdateGlobalSettingsRequest.php create mode 100644 app/Domains/Admin/Actions/UpdateGlobalSettings/UpdateGlobalSettingsResponse.php create mode 100644 app/Domains/Admin/Controllers/GlobalSettingsGetController.php create mode 100644 app/Domains/Admin/Controllers/GlobalSettingsPageController.php create mode 100644 app/Domains/Admin/Controllers/GlobalSettingsUpdateController.php create mode 100644 app/Domains/Admin/Controllers/ParentGroupCreateController.php create mode 100644 app/Domains/Admin/Controllers/ParentGroupDeleteController.php create mode 100644 app/Domains/Admin/Controllers/ParentGroupListApiController.php create mode 100644 app/Domains/Admin/Controllers/ParentGroupListPageController.php create mode 100644 app/Domains/Admin/Controllers/ParentGroupUpdateController.php create mode 100644 app/Domains/Admin/Repositories/AdminParentGroupRepository.php create mode 100644 app/Domains/Admin/Views/GlobalSettings.vue create mode 100644 app/Domains/Admin/Views/ParentGroupList.vue create mode 100644 app/Enumerations/ParticipantGroupType.php create mode 100644 app/Models/GlobalSetting.php create mode 100644 app/Models/ParentGroup.php create mode 100644 app/Repositories/GlobalSettingRepository.php create mode 100644 database/migrations/2026_09_29_140010_create_parent_groups.php create mode 100644 tests/Feature/ParentGroupTest.php diff --git a/CLAUDE.md b/CLAUDE.md index 4ef37b8..5db148a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,6 +16,26 @@ Routing, Mails) stehen in `.ai/conventions.md` und werden hier eingebunden — * - [Zahlungsmodule](app/EventPaymentModules/CLAUDE.md) — interface-gesteuerte Zahlungsarten (Optionen, Config, 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. + ## Umsatzsteuer / Steuerinformationen Rechtlich korrekter Begriff ist **Umsatzsteuer (USt)**, nicht MwSt. Ziel: tagesbasierte Anmelde-Rechnungen („Teilnahme diff --git a/app/Domains/Admin/Actions/CreateParentGroup/CreateParentGroupAction.php b/app/Domains/Admin/Actions/CreateParentGroup/CreateParentGroupAction.php new file mode 100644 index 0000000..2565485 --- /dev/null +++ b/app/Domains/Admin/Actions/CreateParentGroup/CreateParentGroupAction.php @@ -0,0 +1,36 @@ +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; + } +} diff --git a/app/Domains/Admin/Actions/CreateParentGroup/CreateParentGroupRequest.php b/app/Domains/Admin/Actions/CreateParentGroup/CreateParentGroupRequest.php new file mode 100644 index 0000000..489f4ba --- /dev/null +++ b/app/Domains/Admin/Actions/CreateParentGroup/CreateParentGroupRequest.php @@ -0,0 +1,11 @@ +request->parentGroup === null) { + $response->message = sprintf('Bitte bei „%s" eine Auswahl treffen.', ParticipantGroupType::ParentGroup->label()); + + return $response; + } + $tenant = Tenant::create([ 'name' => $this->request->name, 'slug' => $this->request->slug, 'url' => $this->request->url, + 'parent_group_id' => $this->request->parentGroup->id, 'email' => '', 'email_finance' => '', 'account_name' => '', @@ -46,7 +54,7 @@ class CreateTenantAction } $response->success = true; - $response->message = 'Stamm wurde angelegt.'; + $response->message = sprintf('%s wurde angelegt.', ParticipantGroupType::LocalGroup->label()); $response->slug = $tenant->slug; return $response; diff --git a/app/Domains/Admin/Actions/CreateTenant/CreateTenantRequest.php b/app/Domains/Admin/Actions/CreateTenant/CreateTenantRequest.php index 85e7e82..440475e 100644 --- a/app/Domains/Admin/Actions/CreateTenant/CreateTenantRequest.php +++ b/app/Domains/Admin/Actions/CreateTenant/CreateTenantRequest.php @@ -2,12 +2,16 @@ namespace App\Domains\Admin\Actions\CreateTenant; +use App\Models\ParentGroup; + class CreateTenantRequest { public function __construct( public string $name, public string $slug, public string $url, + /** Pflicht; null heißt, die übermittelte ID war unbekannt. */ + public ?ParentGroup $parentGroup, ) { } } diff --git a/app/Domains/Admin/Actions/DeleteParentGroup/DeleteParentGroupAction.php b/app/Domains/Admin/Actions/DeleteParentGroup/DeleteParentGroupAction.php new file mode 100644 index 0000000..c2b30b0 --- /dev/null +++ b/app/Domains/Admin/Actions/DeleteParentGroup/DeleteParentGroupAction.php @@ -0,0 +1,38 @@ +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; + } +} diff --git a/app/Domains/Admin/Actions/DeleteParentGroup/DeleteParentGroupRequest.php b/app/Domains/Admin/Actions/DeleteParentGroup/DeleteParentGroupRequest.php new file mode 100644 index 0000000..b6368ff --- /dev/null +++ b/app/Domains/Admin/Actions/DeleteParentGroup/DeleteParentGroupRequest.php @@ -0,0 +1,15 @@ +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; + } +} diff --git a/app/Domains/Admin/Actions/RenameParentGroup/RenameParentGroupRequest.php b/app/Domains/Admin/Actions/RenameParentGroup/RenameParentGroupRequest.php new file mode 100644 index 0000000..1d7418b --- /dev/null +++ b/app/Domains/Admin/Actions/RenameParentGroup/RenameParentGroupRequest.php @@ -0,0 +1,14 @@ + 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; + } +} diff --git a/app/Domains/Admin/Actions/UpdateGlobalSettings/UpdateGlobalSettingsRequest.php b/app/Domains/Admin/Actions/UpdateGlobalSettings/UpdateGlobalSettingsRequest.php new file mode 100644 index 0000000..196c479 --- /dev/null +++ b/app/Domains/Admin/Actions/UpdateGlobalSettings/UpdateGlobalSettingsRequest.php @@ -0,0 +1,17 @@ +request->parentGroup === null) { + $response->message = sprintf('Bitte bei „%s" eine Auswahl treffen.', ParticipantGroupType::ParentGroup->label()); + + return $response; + } + $this->request->tenant->update([ 'name' => $this->request->name, 'slug' => $this->request->slug, 'url' => $this->request->url, + 'parent_group_id' => $this->request->parentGroup->id, ]); $response->success = true; diff --git a/app/Domains/Admin/Actions/UpdateTenantGeneral/UpdateTenantGeneralRequest.php b/app/Domains/Admin/Actions/UpdateTenantGeneral/UpdateTenantGeneralRequest.php index 277cf0e..195ac89 100644 --- a/app/Domains/Admin/Actions/UpdateTenantGeneral/UpdateTenantGeneralRequest.php +++ b/app/Domains/Admin/Actions/UpdateTenantGeneral/UpdateTenantGeneralRequest.php @@ -2,6 +2,7 @@ namespace App\Domains\Admin\Actions\UpdateTenantGeneral; +use App\Models\ParentGroup; use App\Models\Tenant; class UpdateTenantGeneralRequest @@ -11,6 +12,8 @@ class UpdateTenantGeneralRequest public string $name, public string $slug, public string $url, + /** Pflicht; null heißt, die übermittelte ID war unbekannt. */ + public ?ParentGroup $parentGroup, ) { } } diff --git a/app/Domains/Admin/Controllers/GlobalSettingsGetController.php b/app/Domains/Admin/Controllers/GlobalSettingsGetController.php new file mode 100644 index 0000000..e2c3065 --- /dev/null +++ b/app/Domains/Admin/Controllers/GlobalSettingsGetController.php @@ -0,0 +1,18 @@ +json([ + 'groupLabels' => ParticipantGroupType::labels(), + ]); + } +} diff --git a/app/Domains/Admin/Controllers/GlobalSettingsPageController.php b/app/Domains/Admin/Controllers/GlobalSettingsPageController.php new file mode 100644 index 0000000..0dad486 --- /dev/null +++ b/app/Domains/Admin/Controllers/GlobalSettingsPageController.php @@ -0,0 +1,16 @@ +render(); + } +} diff --git a/app/Domains/Admin/Controllers/GlobalSettingsUpdateController.php b/app/Domains/Admin/Controllers/GlobalSettingsUpdateController.php new file mode 100644 index 0000000..1721925 --- /dev/null +++ b/app/Domains/Admin/Controllers/GlobalSettingsUpdateController.php @@ -0,0 +1,28 @@ +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, + ]); + } +} diff --git a/app/Domains/Admin/Controllers/ParentGroupCreateController.php b/app/Domains/Admin/Controllers/ParentGroupCreateController.php new file mode 100644 index 0000000..c9087e8 --- /dev/null +++ b/app/Domains/Admin/Controllers/ParentGroupCreateController.php @@ -0,0 +1,24 @@ +input('name'), + ))->execute(); + + return response()->json([ + 'status' => $response->success ? 'success' : 'error', + 'message' => $response->message, + ]); + } +} diff --git a/app/Domains/Admin/Controllers/ParentGroupDeleteController.php b/app/Domains/Admin/Controllers/ParentGroupDeleteController.php new file mode 100644 index 0000000..ca73fdf --- /dev/null +++ b/app/Domains/Admin/Controllers/ParentGroupDeleteController.php @@ -0,0 +1,31 @@ +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, + ]); + } +} diff --git a/app/Domains/Admin/Controllers/ParentGroupListApiController.php b/app/Domains/Admin/Controllers/ParentGroupListApiController.php new file mode 100644 index 0000000..07aaf4e --- /dev/null +++ b/app/Domains/Admin/Controllers/ParentGroupListApiController.php @@ -0,0 +1,23 @@ +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]); + } +} diff --git a/app/Domains/Admin/Controllers/ParentGroupListPageController.php b/app/Domains/Admin/Controllers/ParentGroupListPageController.php new file mode 100644 index 0000000..8f9cc42 --- /dev/null +++ b/app/Domains/Admin/Controllers/ParentGroupListPageController.php @@ -0,0 +1,19 @@ + ParticipantGroupType::labels(), + ])->render(); + } +} diff --git a/app/Domains/Admin/Controllers/ParentGroupUpdateController.php b/app/Domains/Admin/Controllers/ParentGroupUpdateController.php new file mode 100644 index 0000000..8eb00f7 --- /dev/null +++ b/app/Domains/Admin/Controllers/ParentGroupUpdateController.php @@ -0,0 +1,31 @@ +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, + ]); + } +} diff --git a/app/Domains/Admin/Controllers/TenantCreateController.php b/app/Domains/Admin/Controllers/TenantCreateController.php index 1048f63..4661219 100644 --- a/app/Domains/Admin/Controllers/TenantCreateController.php +++ b/app/Domains/Admin/Controllers/TenantCreateController.php @@ -16,6 +16,7 @@ class TenantCreateController extends CommonController name: $request->input('name'), slug: $request->input('slug'), url: $request->input('url'), + parentGroup: $this->adminParentGroups->findById((int) $request->input('parent_group_id')), )); $response = $action->execute(); diff --git a/app/Domains/Admin/Controllers/TenantGeneralGetController.php b/app/Domains/Admin/Controllers/TenantGeneralGetController.php index 714bb72..201ff90 100644 --- a/app/Domains/Admin/Controllers/TenantGeneralGetController.php +++ b/app/Domains/Admin/Controllers/TenantGeneralGetController.php @@ -2,6 +2,7 @@ namespace App\Domains\Admin\Controllers; +use App\Enumerations\ParticipantGroupType; use App\Scopes\CommonController; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -17,6 +18,12 @@ class TenantGeneralGetController extends CommonController 'slug' => $tenant->slug, 'url' => $tenant->url, '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', ]); } diff --git a/app/Domains/Admin/Controllers/TenantGeneralUpdateController.php b/app/Domains/Admin/Controllers/TenantGeneralUpdateController.php index a1654fe..6de3a30 100644 --- a/app/Domains/Admin/Controllers/TenantGeneralUpdateController.php +++ b/app/Domains/Admin/Controllers/TenantGeneralUpdateController.php @@ -19,6 +19,7 @@ class TenantGeneralUpdateController extends CommonController name: $request->input('name'), slug: $request->input('slug'), url: $request->input('url'), + parentGroup: $this->adminParentGroups->findById((int) $request->input('parent_group_id')), )); $response = $action->execute(); diff --git a/app/Domains/Admin/Controllers/TenantListApiController.php b/app/Domains/Admin/Controllers/TenantListApiController.php index f09e6b4..0e64692 100644 --- a/app/Domains/Admin/Controllers/TenantListApiController.php +++ b/app/Domains/Admin/Controllers/TenantListApiController.php @@ -18,9 +18,17 @@ class TenantListApiController extends CommonController 'slug' => $tenant->slug, 'url' => $tenant->url, '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]); } } diff --git a/app/Domains/Admin/Controllers/TenantListPageController.php b/app/Domains/Admin/Controllers/TenantListPageController.php index 2cfbeb7..4a1581a 100644 --- a/app/Domains/Admin/Controllers/TenantListPageController.php +++ b/app/Domains/Admin/Controllers/TenantListPageController.php @@ -2,6 +2,7 @@ namespace App\Domains\Admin\Controllers; +use App\Enumerations\ParticipantGroupType; use App\Providers\InertiaProvider; use App\Scopes\CommonController; use Illuminate\Http\Request; @@ -11,7 +12,9 @@ class TenantListPageController extends CommonController { public function __invoke(Request $request): Response { - $inertiaProvider = new InertiaProvider('Admin/TenantList', []); + $inertiaProvider = new InertiaProvider('Admin/TenantList', [ + 'groupLabels' => ParticipantGroupType::labels(), + ]); return $inertiaProvider->render(); } } diff --git a/app/Domains/Admin/Repositories/AdminParentGroupRepository.php b/app/Domains/Admin/Repositories/AdminParentGroupRepository.php new file mode 100644 index 0000000..548cb09 --- /dev/null +++ b/app/Domains/Admin/Repositories/AdminParentGroupRepository.php @@ -0,0 +1,32 @@ +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(); + } +} diff --git a/app/Domains/Admin/Routes/api.php b/app/Domains/Admin/Routes/api.php index f3f94b2..391efae 100644 --- a/app/Domains/Admin/Routes/api.php +++ b/app/Domains/Admin/Routes/api.php @@ -5,6 +5,8 @@ use App\Domains\Admin\Controllers\DocumentAssetUpdateController; use App\Domains\Admin\Controllers\DocumentTemplatesGetController; use App\Domains\Admin\Controllers\DocumentTemplatesPreviewController; 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\ManagedTenantContactUpdateController; 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\ManagedTenantTaxGetController; 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\TenantContactUpdateController; use App\Domains\Admin\Controllers\TenantCreateController; @@ -76,6 +82,12 @@ Route::middleware([IdentifyTenant::class, 'auth', AdminRoleMiddleware::class])-> Route::post('/assets', DocumentAssetUpdateController::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 () { @@ -97,5 +109,12 @@ Route::middleware([IdentifyTenant::class, 'auth', AdminRoleMiddleware::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'); + }); }); }); diff --git a/app/Domains/Admin/Routes/web.php b/app/Domains/Admin/Routes/web.php index 73ca343..952da52 100644 --- a/app/Domains/Admin/Routes/web.php +++ b/app/Domains/Admin/Routes/web.php @@ -2,6 +2,8 @@ use App\Domains\Admin\Controllers\AdminDashboardController; 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\TenantListPageController; 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. Route::middleware(MainAdminRoleMiddleware::class)->group(function () { Route::get('/document-templates', DocumentTemplatesPageController::class); + Route::get('/settings', GlobalSettingsPageController::class); }); Route::middleware(LvOnlyMiddleware::class)->group(function () { Route::get('/tenants', TenantListPageController::class); Route::get('/tenants/{slug}', TenantEditPageController::class); + Route::get('/parent-groups', ParentGroupListPageController::class); }); }); }); diff --git a/app/Domains/Admin/Views/GlobalSettings.vue b/app/Domains/Admin/Views/GlobalSettings.vue new file mode 100644 index 0000000..e753948 --- /dev/null +++ b/app/Domains/Admin/Views/GlobalSettings.vue @@ -0,0 +1,116 @@ + + + + + diff --git a/app/Domains/Admin/Views/ParentGroupList.vue b/app/Domains/Admin/Views/ParentGroupList.vue new file mode 100644 index 0000000..7bfea4c --- /dev/null +++ b/app/Domains/Admin/Views/ParentGroupList.vue @@ -0,0 +1,249 @@ + + + + + diff --git a/app/Domains/Admin/Views/Partials/TenantGeneral.vue b/app/Domains/Admin/Views/Partials/TenantGeneral.vue index bce3d10..51e6271 100644 --- a/app/Domains/Admin/Views/Partials/TenantGeneral.vue +++ b/app/Domains/Admin/Views/Partials/TenantGeneral.vue @@ -1,5 +1,5 @@