diff --git a/app/Domains/Admin/Actions/CreateTenant/CreateTenantAction.php b/app/Domains/Admin/Actions/CreateTenant/CreateTenantAction.php new file mode 100644 index 0000000..05c4504 --- /dev/null +++ b/app/Domains/Admin/Actions/CreateTenant/CreateTenantAction.php @@ -0,0 +1,38 @@ + $this->request->name, + 'slug' => $this->request->slug, + 'url' => $this->request->url, + 'email' => '', + 'email_finance' => '', + 'account_name' => '', + 'account_iban' => '', + 'account_bic' => '', + 'city' => '', + 'postcode' => '', + 'is_active_local_group' => true, + 'has_active_instance' => true, + ]); + + $response->success = true; + $response->message = 'Stamm wurde angelegt.'; + $response->slug = $tenant->slug; + + return $response; + } +} diff --git a/app/Domains/Admin/Actions/CreateTenant/CreateTenantRequest.php b/app/Domains/Admin/Actions/CreateTenant/CreateTenantRequest.php new file mode 100644 index 0000000..85e7e82 --- /dev/null +++ b/app/Domains/Admin/Actions/CreateTenant/CreateTenantRequest.php @@ -0,0 +1,13 @@ +request->currentUserId === $this->request->user->id) { + $response->message = 'Du kannst dich nicht selbst deaktivieren.'; + return $response; + } + + $this->request->user->update(['active' => !$this->request->user->active]); + + $status = $this->request->user->active ? 'aktiviert' : 'deaktiviert'; + + $response->success = true; + $response->message = 'Benutzer*in wurde ' . $status . '.'; + $response->active = $this->request->user->active; + + return $response; + } +} diff --git a/app/Domains/Admin/Actions/ToggleUserActive/ToggleUserActiveRequest.php b/app/Domains/Admin/Actions/ToggleUserActive/ToggleUserActiveRequest.php new file mode 100644 index 0000000..b4f39d5 --- /dev/null +++ b/app/Domains/Admin/Actions/ToggleUserActive/ToggleUserActiveRequest.php @@ -0,0 +1,14 @@ +request->tenant->update([ + 'email' => $this->request->email, + 'email_finance' => $this->request->emailFinance, + 'postcode' => $this->request->postcode, + 'city' => $this->request->city, + ]); + + $response->success = true; + $response->message = 'Kontaktdaten wurden gespeichert.'; + + return $response; + } +} diff --git a/app/Domains/Admin/Actions/UpdateTenantContact/UpdateTenantContactRequest.php b/app/Domains/Admin/Actions/UpdateTenantContact/UpdateTenantContactRequest.php new file mode 100644 index 0000000..f9135ea --- /dev/null +++ b/app/Domains/Admin/Actions/UpdateTenantContact/UpdateTenantContactRequest.php @@ -0,0 +1,17 @@ +request->tenant->update([ + 'gdpr_text' => $this->request->gdprText, + ]); + + $response->success = true; + $response->message = 'Datenschutzerklärung wurde gespeichert.'; + + return $response; + } +} diff --git a/app/Domains/Admin/Actions/UpdateTenantGdpr/UpdateTenantGdprRequest.php b/app/Domains/Admin/Actions/UpdateTenantGdpr/UpdateTenantGdprRequest.php new file mode 100644 index 0000000..4e63c27 --- /dev/null +++ b/app/Domains/Admin/Actions/UpdateTenantGdpr/UpdateTenantGdprRequest.php @@ -0,0 +1,14 @@ +request->tenant->update([ + 'name' => $this->request->name, + 'slug' => $this->request->slug, + 'url' => $this->request->url, + ]); + + $response->success = true; + $response->message = 'Allgemeine Daten wurden gespeichert.'; + + return $response; + } +} diff --git a/app/Domains/Admin/Actions/UpdateTenantGeneral/UpdateTenantGeneralRequest.php b/app/Domains/Admin/Actions/UpdateTenantGeneral/UpdateTenantGeneralRequest.php new file mode 100644 index 0000000..277cf0e --- /dev/null +++ b/app/Domains/Admin/Actions/UpdateTenantGeneral/UpdateTenantGeneralRequest.php @@ -0,0 +1,16 @@ +request->tenant->update([ + 'impress_text' => $this->request->impressText, + ]); + + $response->success = true; + $response->message = 'Impressum wurde gespeichert.'; + + return $response; + } +} diff --git a/app/Domains/Admin/Actions/UpdateTenantImpress/UpdateTenantImpressRequest.php b/app/Domains/Admin/Actions/UpdateTenantImpress/UpdateTenantImpressRequest.php new file mode 100644 index 0000000..d3e8637 --- /dev/null +++ b/app/Domains/Admin/Actions/UpdateTenantImpress/UpdateTenantImpressRequest.php @@ -0,0 +1,14 @@ +request->tenant->update([ + 'account_iban' => $this->request->accountIban, + 'account_bic' => $this->request->accountBic, + 'account_name' => $this->request->accountName, + ]); + + $response->success = true; + $response->message = 'Bezahldaten wurden gespeichert.'; + + return $response; + } +} diff --git a/app/Domains/Admin/Actions/UpdateTenantPayment/UpdateTenantPaymentRequest.php b/app/Domains/Admin/Actions/UpdateTenantPayment/UpdateTenantPaymentRequest.php new file mode 100644 index 0000000..1bc9a60 --- /dev/null +++ b/app/Domains/Admin/Actions/UpdateTenantPayment/UpdateTenantPaymentRequest.php @@ -0,0 +1,16 @@ +request->isLvTenant) { + $allowedFields[] = 'local_group'; + if (!$this->request->isOwnUser) { + $allowedFields[] = 'user_role_main'; + } + } + + $data = array_intersect_key($this->request->data, array_flip($allowedFields)); + $this->request->user->update($data); + + $response->success = true; + $response->message = 'Benutzerdaten wurden gespeichert.'; + + return $response; + } +} diff --git a/app/Domains/Admin/Actions/UpdateUser/UpdateUserRequest.php b/app/Domains/Admin/Actions/UpdateUser/UpdateUserRequest.php new file mode 100644 index 0000000..85a347a --- /dev/null +++ b/app/Domains/Admin/Actions/UpdateUser/UpdateUserRequest.php @@ -0,0 +1,16 @@ +render(); + } +} diff --git a/app/Domains/Admin/Controllers/ManagedTenantContactGetController.php b/app/Domains/Admin/Controllers/ManagedTenantContactGetController.php new file mode 100644 index 0000000..cb35234 --- /dev/null +++ b/app/Domains/Admin/Controllers/ManagedTenantContactGetController.php @@ -0,0 +1,23 @@ +adminTenants->findBySlug($slug); + + return response()->json([ + 'email' => $tenant->email, + 'email_finance' => $tenant->email_finance, + 'postcode' => $tenant->postcode, + 'city' => $tenant->city, + 'saveEndpoint' => '/api/v1/admin/tenants/' . $slug . '/contact', + ]); + } +} diff --git a/app/Domains/Admin/Controllers/ManagedTenantContactUpdateController.php b/app/Domains/Admin/Controllers/ManagedTenantContactUpdateController.php new file mode 100644 index 0000000..dce1996 --- /dev/null +++ b/app/Domains/Admin/Controllers/ManagedTenantContactUpdateController.php @@ -0,0 +1,32 @@ +adminTenants->findBySlug($slug); + + $action = new UpdateTenantContactAction(new UpdateTenantContactRequest( + tenant: $tenant, + email: $request->input('email'), + emailFinance: $request->input('email_finance'), + postcode: $request->input('postcode'), + city: $request->input('city'), + )); + + $response = $action->execute(); + + return response()->json([ + 'status' => $response->success ? 'success' : 'error', + 'message' => $response->message, + ]); + } +} diff --git a/app/Domains/Admin/Controllers/ManagedTenantGdprGetController.php b/app/Domains/Admin/Controllers/ManagedTenantGdprGetController.php new file mode 100644 index 0000000..06bd941 --- /dev/null +++ b/app/Domains/Admin/Controllers/ManagedTenantGdprGetController.php @@ -0,0 +1,20 @@ +adminTenants->findBySlug($slug); + + return response()->json([ + 'gdpr_text' => $tenant->gdpr_text ?? '', + 'saveEndpoint' => '/api/v1/admin/tenants/' . $slug . '/gdpr', + ]); + } +} diff --git a/app/Domains/Admin/Controllers/ManagedTenantGdprUpdateController.php b/app/Domains/Admin/Controllers/ManagedTenantGdprUpdateController.php new file mode 100644 index 0000000..47703c5 --- /dev/null +++ b/app/Domains/Admin/Controllers/ManagedTenantGdprUpdateController.php @@ -0,0 +1,30 @@ +adminTenants->findBySlug($slug); + + $action = new UpdateTenantGdprAction(new UpdateTenantGdprRequest( + tenant: $tenant, + gdprText: $request->input('gdpr_text'), + )); + + $response = $action->execute(); + + return response()->json([ + 'status' => $response->success ? 'success' : 'error', + 'message' => $response->message, + ]); + } +} diff --git a/app/Domains/Admin/Controllers/ManagedTenantImpressGetController.php b/app/Domains/Admin/Controllers/ManagedTenantImpressGetController.php new file mode 100644 index 0000000..8a718da --- /dev/null +++ b/app/Domains/Admin/Controllers/ManagedTenantImpressGetController.php @@ -0,0 +1,20 @@ +adminTenants->findBySlug($slug); + + return response()->json([ + 'impress_text' => $tenant->impress_text ?? '', + 'saveEndpoint' => '/api/v1/admin/tenants/' . $slug . '/impress', + ]); + } +} diff --git a/app/Domains/Admin/Controllers/ManagedTenantImpressUpdateController.php b/app/Domains/Admin/Controllers/ManagedTenantImpressUpdateController.php new file mode 100644 index 0000000..baa1225 --- /dev/null +++ b/app/Domains/Admin/Controllers/ManagedTenantImpressUpdateController.php @@ -0,0 +1,29 @@ +adminTenants->findBySlug($slug); + + $action = new UpdateTenantImpressAction(new UpdateTenantImpressRequest( + tenant: $tenant, + impressText: $request->input('impress_text'), + )); + + $response = $action->execute(); + + return response()->json([ + 'status' => $response->success ? 'success' : 'error', + 'message' => $response->message, + ]); + } +} diff --git a/app/Domains/Admin/Controllers/ManagedTenantPaymentGetController.php b/app/Domains/Admin/Controllers/ManagedTenantPaymentGetController.php new file mode 100644 index 0000000..e57e10b --- /dev/null +++ b/app/Domains/Admin/Controllers/ManagedTenantPaymentGetController.php @@ -0,0 +1,22 @@ +adminTenants->findBySlug($slug); + + return response()->json([ + 'account_iban' => $tenant->account_iban, + 'account_bic' => $tenant->account_bic, + 'account_name' => $tenant->account_name, + 'saveEndpoint' => '/api/v1/admin/tenants/' . $slug . '/payment', + ]); + } +} diff --git a/app/Domains/Admin/Controllers/ManagedTenantPaymentUpdateController.php b/app/Domains/Admin/Controllers/ManagedTenantPaymentUpdateController.php new file mode 100644 index 0000000..8d8e184 --- /dev/null +++ b/app/Domains/Admin/Controllers/ManagedTenantPaymentUpdateController.php @@ -0,0 +1,31 @@ +adminTenants->findBySlug($slug); + + $action = new UpdateTenantPaymentAction(new UpdateTenantPaymentRequest( + tenant: $tenant, + accountIban: $request->input('account_iban'), + accountBic: $request->input('account_bic'), + accountName: $request->input('account_name'), + )); + + $response = $action->execute(); + + return response()->json([ + 'status' => $response->success ? 'success' : 'error', + 'message' => $response->message, + ]); + } +} diff --git a/app/Domains/Admin/Controllers/TenantContactGetController.php b/app/Domains/Admin/Controllers/TenantContactGetController.php new file mode 100644 index 0000000..b660d45 --- /dev/null +++ b/app/Domains/Admin/Controllers/TenantContactGetController.php @@ -0,0 +1,21 @@ +json([ + 'email' => $this->tenant->email, + 'email_finance' => $this->tenant->email_finance, + 'postcode' => $this->tenant->postcode, + 'city' => $this->tenant->city, + 'saveEndpoint' => '/api/v1/admin/tenant/contact', + ]); + } +} diff --git a/app/Domains/Admin/Controllers/TenantContactUpdateController.php b/app/Domains/Admin/Controllers/TenantContactUpdateController.php new file mode 100644 index 0000000..bc38a23 --- /dev/null +++ b/app/Domains/Admin/Controllers/TenantContactUpdateController.php @@ -0,0 +1,30 @@ +tenant, + email: $request->input('email'), + emailFinance: $request->input('email_finance'), + postcode: $request->input('postcode'), + city: $request->input('city'), + )); + + $response = $action->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 new file mode 100644 index 0000000..1048f63 --- /dev/null +++ b/app/Domains/Admin/Controllers/TenantCreateController.php @@ -0,0 +1,29 @@ +input('name'), + slug: $request->input('slug'), + url: $request->input('url'), + )); + + $response = $action->execute(); + + return response()->json([ + 'status' => $response->success ? 'success' : 'error', + 'message' => $response->message, + 'slug' => $response->slug, + ]); + } +} diff --git a/app/Domains/Admin/Controllers/TenantEditPageController.php b/app/Domains/Admin/Controllers/TenantEditPageController.php new file mode 100644 index 0000000..d56262d --- /dev/null +++ b/app/Domains/Admin/Controllers/TenantEditPageController.php @@ -0,0 +1,26 @@ +adminTenants->findBySlug($slug); + + $inertiaProvider = new InertiaProvider('Admin/TenantEdit', [ + 'tenant' => [ + 'name' => $tenant->name, + 'slug' => $tenant->slug, + 'url' => $tenant->url, + 'is_active_local_group' => $tenant->is_active_local_group, + ], + ]); + return $inertiaProvider->render(); + } +} diff --git a/app/Domains/Admin/Controllers/TenantGdprGetController.php b/app/Domains/Admin/Controllers/TenantGdprGetController.php new file mode 100644 index 0000000..d98f35f --- /dev/null +++ b/app/Domains/Admin/Controllers/TenantGdprGetController.php @@ -0,0 +1,18 @@ +json([ + 'gdpr_text' => $this->tenant->gdpr_text ?? '', + 'saveEndpoint' => '/api/v1/admin/tenant/gdpr', + ]); + } +} diff --git a/app/Domains/Admin/Controllers/TenantGdprUpdateController.php b/app/Domains/Admin/Controllers/TenantGdprUpdateController.php new file mode 100644 index 0000000..e9886d8 --- /dev/null +++ b/app/Domains/Admin/Controllers/TenantGdprUpdateController.php @@ -0,0 +1,27 @@ +tenant, + gdprText: $request->input('gdpr_text'), + )); + + $response = $action->execute(); + + return response()->json([ + 'status' => $response->success ? 'success' : 'error', + 'message' => $response->message, + ]); + } +} diff --git a/app/Domains/Admin/Controllers/TenantGeneralGetController.php b/app/Domains/Admin/Controllers/TenantGeneralGetController.php new file mode 100644 index 0000000..714bb72 --- /dev/null +++ b/app/Domains/Admin/Controllers/TenantGeneralGetController.php @@ -0,0 +1,23 @@ +adminTenants->findBySlug($slug); + + return response()->json([ + 'name' => $tenant->name, + 'slug' => $tenant->slug, + 'url' => $tenant->url, + 'is_active_local_group' => $tenant->is_active_local_group, + 'saveEndpoint' => '/api/v1/admin/tenants/' . $slug . '/general', + ]); + } +} diff --git a/app/Domains/Admin/Controllers/TenantGeneralUpdateController.php b/app/Domains/Admin/Controllers/TenantGeneralUpdateController.php new file mode 100644 index 0000000..a1654fe --- /dev/null +++ b/app/Domains/Admin/Controllers/TenantGeneralUpdateController.php @@ -0,0 +1,31 @@ +adminTenants->findBySlug($slug); + + $action = new UpdateTenantGeneralAction(new UpdateTenantGeneralRequest( + tenant: $tenant, + name: $request->input('name'), + slug: $request->input('slug'), + url: $request->input('url'), + )); + + $response = $action->execute(); + + return response()->json([ + 'status' => $response->success ? 'success' : 'error', + 'message' => $response->message, + ]); + } +} diff --git a/app/Domains/Admin/Controllers/TenantImpressGetController.php b/app/Domains/Admin/Controllers/TenantImpressGetController.php new file mode 100644 index 0000000..6419db4 --- /dev/null +++ b/app/Domains/Admin/Controllers/TenantImpressGetController.php @@ -0,0 +1,18 @@ +json([ + 'impress_text' => $this->tenant->impress_text ?? '', + 'saveEndpoint' => '/api/v1/admin/tenant/impress', + ]); + } +} diff --git a/app/Domains/Admin/Controllers/TenantImpressUpdateController.php b/app/Domains/Admin/Controllers/TenantImpressUpdateController.php new file mode 100644 index 0000000..b18a1f1 --- /dev/null +++ b/app/Domains/Admin/Controllers/TenantImpressUpdateController.php @@ -0,0 +1,27 @@ +tenant, + impressText: $request->input('impress_text'), + )); + + $response = $action->execute(); + + return response()->json([ + 'status' => $response->success ? 'success' : 'error', + 'message' => $response->message, + ]); + } +} diff --git a/app/Domains/Admin/Controllers/TenantListApiController.php b/app/Domains/Admin/Controllers/TenantListApiController.php new file mode 100644 index 0000000..f09e6b4 --- /dev/null +++ b/app/Domains/Admin/Controllers/TenantListApiController.php @@ -0,0 +1,26 @@ +adminTenants->getActiveTenants(); + + $mapped = $tenants->map(function ($tenant) { + return [ + 'name' => $tenant->name, + 'slug' => $tenant->slug, + 'url' => $tenant->url, + 'is_active_local_group' => $tenant->is_active_local_group, + ]; + }); + + return response()->json(['tenants' => $mapped]); + } +} diff --git a/app/Domains/Admin/Controllers/TenantListPageController.php b/app/Domains/Admin/Controllers/TenantListPageController.php new file mode 100644 index 0000000..2cfbeb7 --- /dev/null +++ b/app/Domains/Admin/Controllers/TenantListPageController.php @@ -0,0 +1,17 @@ +render(); + } +} diff --git a/app/Domains/Admin/Controllers/TenantPageController.php b/app/Domains/Admin/Controllers/TenantPageController.php new file mode 100644 index 0000000..72bd738 --- /dev/null +++ b/app/Domains/Admin/Controllers/TenantPageController.php @@ -0,0 +1,24 @@ + [ + 'name' => $this->tenant->name, + 'slug' => $this->tenant->slug, + 'url' => $this->tenant->url, + 'is_active_local_group' => $this->tenant->is_active_local_group, + ], + ]); + return $inertiaProvider->render(); + } +} diff --git a/app/Domains/Admin/Controllers/TenantPaymentGetController.php b/app/Domains/Admin/Controllers/TenantPaymentGetController.php new file mode 100644 index 0000000..1fc3a86 --- /dev/null +++ b/app/Domains/Admin/Controllers/TenantPaymentGetController.php @@ -0,0 +1,20 @@ +json([ + 'account_iban' => $this->tenant->account_iban, + 'account_bic' => $this->tenant->account_bic, + 'account_name' => $this->tenant->account_name, + 'saveEndpoint' => '/api/v1/admin/tenant/payment', + ]); + } +} diff --git a/app/Domains/Admin/Controllers/TenantPaymentUpdateController.php b/app/Domains/Admin/Controllers/TenantPaymentUpdateController.php new file mode 100644 index 0000000..f6bf901 --- /dev/null +++ b/app/Domains/Admin/Controllers/TenantPaymentUpdateController.php @@ -0,0 +1,29 @@ +tenant, + accountIban: $request->input('account_iban'), + accountBic: $request->input('account_bic'), + accountName: $request->input('account_name'), + )); + + $response = $action->execute(); + + return response()->json([ + 'status' => $response->success ? 'success' : 'error', + 'message' => $response->message, + ]); + } +} diff --git a/app/Domains/Admin/Controllers/UserDetailGetController.php b/app/Domains/Admin/Controllers/UserDetailGetController.php new file mode 100644 index 0000000..a093c0d --- /dev/null +++ b/app/Domains/Admin/Controllers/UserDetailGetController.php @@ -0,0 +1,32 @@ +adminUsers->findById($id); + + $userData = $user->toArray(); + unset($userData['password'], $userData['remember_token'], $userData['activation_token'], $userData['activation_token_expires_at']); + + $tenantNames = $this->adminTenants->getTenantNames(); + $userData['nicename'] = $user->getNicename(); + $userData['fullname'] = $user->getFullName(); + $userData['local_group_name'] = $tenantNames[$user->local_group] ?? $user->local_group; + + return response()->json([ + 'user' => $userData, + 'isOwnUser' => auth()->id() === $user->id, + 'isLvTenant' => $this->tenant->slug === 'lv', + 'userRoles' => UserRole::all()->map(fn($role) => ['slug' => $role->slug, 'name' => $role->name]), + 'localGroups' => $this->adminTenants->getActiveLocalGroups()->map(fn($t) => ['slug' => $t->slug, 'name' => $t->name]), + ]); + } +} diff --git a/app/Domains/Admin/Controllers/UserListApiController.php b/app/Domains/Admin/Controllers/UserListApiController.php new file mode 100644 index 0000000..8ba8a76 --- /dev/null +++ b/app/Domains/Admin/Controllers/UserListApiController.php @@ -0,0 +1,30 @@ +adminTenants->getTenantNames(); + $users = $this->adminUsers->getListForTenant($this->tenant->slug); + + $mapped = $users->map(function ($user) use ($tenantNames) { + return [ + 'id' => $user->id, + 'firstname' => $user->firstname, + 'lastname' => $user->lastname, + 'nickname' => $user->nickname, + 'local_group' => $user->local_group, + 'local_group_name' => $tenantNames[$user->local_group] ?? $user->local_group, + 'active' => $user->active, + ]; + }); + + return response()->json(['users' => $mapped]); + } +} diff --git a/app/Domains/Admin/Controllers/UserListPageController.php b/app/Domains/Admin/Controllers/UserListPageController.php new file mode 100644 index 0000000..1ebc516 --- /dev/null +++ b/app/Domains/Admin/Controllers/UserListPageController.php @@ -0,0 +1,19 @@ + $this->tenant->slug === 'lv', + ]); + return $inertiaProvider->render(); + } +} diff --git a/app/Domains/Admin/Controllers/UserResetPasswordController.php b/app/Domains/Admin/Controllers/UserResetPasswordController.php new file mode 100644 index 0000000..d27ebb8 --- /dev/null +++ b/app/Domains/Admin/Controllers/UserResetPasswordController.php @@ -0,0 +1,31 @@ +adminUsers->findById($id); + + if (!$user->email) { + return response()->json([ + 'status' => 'error', + 'message' => 'Benutzer*in hat keine E-Mail-Adresse hinterlegt.', + ]); + } + + $command = new GenerateActivationTokenCommand($user); + $command->execute(); + + return response()->json([ + 'status' => 'success', + 'message' => 'Passwort-Reset-Mail wurde gesendet.', + ]); + } +} diff --git a/app/Domains/Admin/Controllers/UserToggleActiveController.php b/app/Domains/Admin/Controllers/UserToggleActiveController.php new file mode 100644 index 0000000..005d7bc --- /dev/null +++ b/app/Domains/Admin/Controllers/UserToggleActiveController.php @@ -0,0 +1,30 @@ +adminUsers->findById($id); + + $action = new ToggleUserActiveAction(new ToggleUserActiveRequest( + user: $user, + currentUserId: auth()->id(), + )); + + $response = $action->execute(); + + return response()->json([ + 'status' => $response->success ? 'success' : 'error', + 'message' => $response->message, + 'active' => $response->active, + ]); + } +} diff --git a/app/Domains/Admin/Controllers/UserUpdateController.php b/app/Domains/Admin/Controllers/UserUpdateController.php new file mode 100644 index 0000000..6878692 --- /dev/null +++ b/app/Domains/Admin/Controllers/UserUpdateController.php @@ -0,0 +1,31 @@ +adminUsers->findById($id); + + $action = new UpdateUserAction(new UpdateUserRequest( + user: $user, + data: $request->all(), + isOwnUser: auth()->id() === $user->id, + isLvTenant: $this->tenant->slug === 'lv', + )); + + $response = $action->execute(); + + return response()->json([ + 'status' => $response->success ? 'success' : 'error', + 'message' => $response->message, + ]); + } +} diff --git a/app/Domains/Admin/Repositories/AdminTenantRepository.php b/app/Domains/Admin/Repositories/AdminTenantRepository.php new file mode 100644 index 0000000..0c92d38 --- /dev/null +++ b/app/Domains/Admin/Repositories/AdminTenantRepository.php @@ -0,0 +1,30 @@ +firstOrFail(); + } + + public function getActiveTenants(): Collection + { + return Tenant::where('has_active_instance', true)->get(); + } + + public function getTenantNames(): SupportCollection + { + return Tenant::pluck('name', 'slug'); + } + + public function getActiveLocalGroups(): Collection + { + return Tenant::where('is_active_local_group', true)->get(); + } +} diff --git a/app/Domains/Admin/Repositories/AdminUserRepository.php b/app/Domains/Admin/Repositories/AdminUserRepository.php new file mode 100644 index 0000000..997ca58 --- /dev/null +++ b/app/Domains/Admin/Repositories/AdminUserRepository.php @@ -0,0 +1,25 @@ +where('local_group', $tenantSlug); + } + + return $query->orderBy('lastname')->orderBy('firstname')->get(); + } +} diff --git a/app/Domains/Admin/Routes/api.php b/app/Domains/Admin/Routes/api.php new file mode 100644 index 0000000..2d1337d --- /dev/null +++ b/app/Domains/Admin/Routes/api.php @@ -0,0 +1,71 @@ +group(function () { + Route::prefix('api/v1/admin/tenant')->group(function () { + Route::get('/contact', TenantContactGetController::class); + Route::post('/contact', TenantContactUpdateController::class); + Route::get('/payment', TenantPaymentGetController::class); + Route::post('/payment', TenantPaymentUpdateController::class); + Route::get('/impress', TenantImpressGetController::class); + Route::post('/impress', TenantImpressUpdateController::class); + Route::get('/gdpr', TenantGdprGetController::class); + Route::post('/gdpr', TenantGdprUpdateController::class); + }); + + Route::prefix('api/v1/admin/users')->group(function () { + Route::get('/list', UserListApiController::class); + Route::get('/{id}', UserDetailGetController::class); + Route::post('/{id}', UserUpdateController::class); + Route::post('/{id}/toggle-active', UserToggleActiveController::class); + Route::post('/{id}/reset-password', UserResetPasswordController::class); + }); + + Route::middleware(LvOnlyMiddleware::class)->group(function () { + Route::prefix('api/v1/admin/tenants')->group(function () { + Route::get('/list', TenantListApiController::class); + Route::post('/create', TenantCreateController::class); + Route::prefix('/{slug}')->group(function () { + Route::get('/general', TenantGeneralGetController::class); + Route::post('/general', TenantGeneralUpdateController::class); + Route::get('/contact', ManagedTenantContactGetController::class); + Route::post('/contact', ManagedTenantContactUpdateController::class); + Route::get('/payment', ManagedTenantPaymentGetController::class); + Route::post('/payment', ManagedTenantPaymentUpdateController::class); + Route::get('/impress', ManagedTenantImpressGetController::class); + Route::post('/impress', ManagedTenantImpressUpdateController::class); + Route::get('/gdpr', ManagedTenantGdprGetController::class); + Route::post('/gdpr', ManagedTenantGdprUpdateController::class); + }); + }); + }); +}); diff --git a/app/Domains/Admin/Routes/web.php b/app/Domains/Admin/Routes/web.php new file mode 100644 index 0000000..793962f --- /dev/null +++ b/app/Domains/Admin/Routes/web.php @@ -0,0 +1,24 @@ +group(function () { + Route::prefix('admin')->group(function () { + Route::get('/', AdminDashboardController::class); + Route::get('/tenant', TenantPageController::class); + Route::get('/users', UserListPageController::class); + + Route::middleware(LvOnlyMiddleware::class)->group(function () { + Route::get('/tenants', TenantListPageController::class); + Route::get('/tenants/{slug}', TenantEditPageController::class); + }); + }); +}); diff --git a/app/Domains/Admin/Views/Dashboard.vue b/app/Domains/Admin/Views/Dashboard.vue new file mode 100644 index 0000000..b403e2b --- /dev/null +++ b/app/Domains/Admin/Views/Dashboard.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/app/Domains/Admin/Views/Partials/TenantContact.vue b/app/Domains/Admin/Views/Partials/TenantContact.vue new file mode 100644 index 0000000..11843a5 --- /dev/null +++ b/app/Domains/Admin/Views/Partials/TenantContact.vue @@ -0,0 +1,146 @@ + + + + + diff --git a/app/Domains/Admin/Views/Partials/TenantGdpr.vue b/app/Domains/Admin/Views/Partials/TenantGdpr.vue new file mode 100644 index 0000000..bf4cb91 --- /dev/null +++ b/app/Domains/Admin/Views/Partials/TenantGdpr.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/app/Domains/Admin/Views/Partials/TenantGeneral.vue b/app/Domains/Admin/Views/Partials/TenantGeneral.vue new file mode 100644 index 0000000..bce3d10 --- /dev/null +++ b/app/Domains/Admin/Views/Partials/TenantGeneral.vue @@ -0,0 +1,157 @@ + + + + + diff --git a/app/Domains/Admin/Views/Partials/TenantImpress.vue b/app/Domains/Admin/Views/Partials/TenantImpress.vue new file mode 100644 index 0000000..89f0799 --- /dev/null +++ b/app/Domains/Admin/Views/Partials/TenantImpress.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/app/Domains/Admin/Views/Partials/TenantPayment.vue b/app/Domains/Admin/Views/Partials/TenantPayment.vue new file mode 100644 index 0000000..82123f6 --- /dev/null +++ b/app/Domains/Admin/Views/Partials/TenantPayment.vue @@ -0,0 +1,140 @@ + + + + + diff --git a/app/Domains/Admin/Views/Partials/UserDetail.vue b/app/Domains/Admin/Views/Partials/UserDetail.vue new file mode 100644 index 0000000..2a0106d --- /dev/null +++ b/app/Domains/Admin/Views/Partials/UserDetail.vue @@ -0,0 +1,331 @@ + + + + + diff --git a/app/Domains/Admin/Views/TenantData.vue b/app/Domains/Admin/Views/TenantData.vue new file mode 100644 index 0000000..5622db5 --- /dev/null +++ b/app/Domains/Admin/Views/TenantData.vue @@ -0,0 +1,109 @@ + + + + + diff --git a/app/Domains/Admin/Views/TenantEdit.vue b/app/Domains/Admin/Views/TenantEdit.vue new file mode 100644 index 0000000..588d66d --- /dev/null +++ b/app/Domains/Admin/Views/TenantEdit.vue @@ -0,0 +1,52 @@ + + + diff --git a/app/Domains/Admin/Views/TenantList.vue b/app/Domains/Admin/Views/TenantList.vue new file mode 100644 index 0000000..9773fc9 --- /dev/null +++ b/app/Domains/Admin/Views/TenantList.vue @@ -0,0 +1,248 @@ + + + + + diff --git a/app/Domains/Admin/Views/UserList.vue b/app/Domains/Admin/Views/UserList.vue new file mode 100644 index 0000000..a325bf9 --- /dev/null +++ b/app/Domains/Admin/Views/UserList.vue @@ -0,0 +1,167 @@ + + + + + diff --git a/app/Domains/CostUnit/Controllers/ExportController.php b/app/Domains/CostUnit/Controllers/ExportController.php index a473033..5d14f4c 100644 --- a/app/Domains/CostUnit/Controllers/ExportController.php +++ b/app/Domains/CostUnit/Controllers/ExportController.php @@ -4,14 +4,13 @@ namespace App\Domains\CostUnit\Controllers; use App\Domains\Invoice\Actions\ChangeStatus\ChangeStatusCommand; use App\Domains\Invoice\Actions\ChangeStatus\ChangeStatusRequest; -use App\Domains\Invoice\Actions\CreateInvoice\CreateInvoiceRequest; use App\Domains\Invoice\Actions\CreateInvoiceReceipt\CreateInvoiceReceiptCommand; use App\Domains\Invoice\Actions\CreateInvoiceReceipt\CreateInvoiceReceiptRequest; use App\Enumerations\InvoiceStatus; +use App\Models\SepaPaymentElement; use App\Models\Tenant; use App\Providers\FileWriteProvider; use App\Providers\InvoiceCsvFileProvider; -use App\Providers\PainFileProvider; use App\Providers\WebDavProvider; use App\Providers\ZipArchiveFileProvider; use App\Scopes\CommonController; @@ -25,24 +24,19 @@ class ExportController extends CommonController { $webdavProvider = new WebDavProvider(WebDavProvider::INVOICE_PREFIX . $this->tenant->url . '/' . $costUnit->name); - $painFileData = $this->painData($invoicesForExport); + $this->createSepaPaymentElements($invoicesForExport, $costUnit); $csvData = $this->csvData($invoicesForExport); $filePrefix = Tenant::getTempDirectory(); - $painFileWriteProvider = new FileWriteProvider($filePrefix . 'abrechnungen-' . date('Y-m-d_H-i') . '-sepa.xml', $painFileData); - $painFileWriteProvider->writeToFile(); - $csvFileWriteProvider = new FileWriteProvider($filePrefix . 'abrechnungen-' . date('Y-m-d_H-i') . '.csv', $csvData); $csvFileWriteProvider->writeToFile(); if ($this->tenant->upload_exports) { - $webdavProvider->uploadFile($painFileWriteProvider->fileName); $webdavProvider->uploadFile($csvFileWriteProvider->fileName); } $downloadZipArchiveFiles = [ - $painFileWriteProvider->fileName, $csvFileWriteProvider->fileName ]; @@ -72,7 +66,6 @@ class ExportController extends CommonController { Storage::delete($file); } - Storage::delete($painFileWriteProvider->fileName); Storage::delete($csvFileWriteProvider->fileName); return response()->download( @@ -82,24 +75,28 @@ class ExportController extends CommonController { ); } - Storage::delete($painFileWriteProvider->fileName); Storage::delete($csvFileWriteProvider->fileName); return response()->json([ - 'message' => 'Die Abrechnungen wurden exportiert.' . PHP_EOL .'Die Belege werden asynchron auf dem Webdav-Server hinterlegt.' . PHP_EOL . PHP_EOL . 'Sollten diese in 15 Minuten nicht vollständig sein, kontaktiere den Adminbistrator.' + 'message' => 'Die Abrechnungen wurden exportiert.' . PHP_EOL . 'Die SEPA-Überweisungsdatei kann über den Tab "Globale Aktionen" in der Kostenstellenübersicht erzeugt werden.' . PHP_EOL . PHP_EOL . 'Die Belege werden asynchron auf dem Webdav-Server hinterlegt.' . PHP_EOL . 'Sollten diese in 15 Minuten nicht vollständig sein, kontaktiere den Administrator.' ]); } - private function painData(array $invoices) : string { - $invoicesForPainFile = []; + private function createSepaPaymentElements(array $invoices, $costUnit): void + { foreach ($invoices as $invoice) { if ($invoice->contact_bank_owner !== null && $invoice->contact_bank_iban !== '' && !$invoice->donation) { - $invoicesForPainFile[] = $invoice; + SepaPaymentElement::create([ + 'tenant' => $this->tenant->slug, + 'invoice_id' => $invoice->id, + 'cost_unit_id' => $costUnit->id, + 'amount' => $invoice->amount, + 'recipient_name' => $invoice->contact_bank_owner, + 'recipient_iban' => $invoice->contact_bank_iban, + 'payment_purpose' => $invoice->payment_purpose ?? 'Auslagenerstattung Rechnungsnummer ' . $invoice->invoice_number, + ]); } } - - $painFileProvider = new PainFileProvider($this->tenant->account_iban, $this->tenant->account_name, $this->tenant->account_bic, $invoicesForPainFile); - return $painFileProvider->createPainFileContent(); } public function csvData(array $invoices) : string { @@ -107,4 +104,3 @@ class ExportController extends CommonController { return $csvDateProvider->createCsvFileContent(); } } - diff --git a/app/Domains/CostUnit/Controllers/GlobalSepaExportController.php b/app/Domains/CostUnit/Controllers/GlobalSepaExportController.php new file mode 100644 index 0000000..5fe3ff3 --- /dev/null +++ b/app/Domains/CostUnit/Controllers/GlobalSepaExportController.php @@ -0,0 +1,82 @@ +getUserRole(); + if (!in_array($role, [UserRole::USER_ROLE_ADMIN, UserRole::USER_ROLE_GROUP_LEADER], true)) { + abort(403); + } + } + + public function getGlobalActions() + { + $this->checkAuthorization(); + + $pendingElements = SepaPaymentElement::where('exported', false)->get(); + $pendingCount = $pendingElements->count(); + $pendingAmount = number_format($pendingElements->sum('amount'), 2, ',', '.'); + + return response()->json([ + 'pending_count' => $pendingCount, + 'pending_amount' => $pendingAmount, + ]); + } + + public function exportSepaFile() + { + $this->checkAuthorization(); + + return DB::transaction(function () { + $elements = SepaPaymentElement::where('exported', false)->lockForUpdate()->get(); + + if ($elements->isEmpty()) { + return response()->json([ + 'message' => 'Es gibt keine ausstehenden SEPA-Überweisungen.' + ], 404); + } + + $painFileProvider = new PainFileProvider( + $this->tenant->account_iban, + $this->tenant->account_name, + $this->tenant->account_bic, + $elements->all() + ); + + $painContent = $painFileProvider->createPainFileContent(); + + $filePrefix = Tenant::getTempDirectory(); + $fileName = $filePrefix . 'sepa-pain-' . date('Y-m-d_H-i') . '.xml'; + + $fileWriteProvider = new FileWriteProvider($fileName, $painContent); + $fileWriteProvider->writeToFile(); + + $elements->each(function (SepaPaymentElement $element) { + $element->update([ + 'exported' => true, + 'exported_at' => now(), + ]); + }); + + $filePath = storage_path('app/private/' . $fileName); + + return response()->download($filePath, basename($fileName), [ + 'Content-Type' => 'application/xml', + ])->deleteFileAfterSend(true); + }); + } +} diff --git a/app/Domains/CostUnit/Routes/api.php b/app/Domains/CostUnit/Routes/api.php index 76781e1..26bf25c 100644 --- a/app/Domains/CostUnit/Routes/api.php +++ b/app/Domains/CostUnit/Routes/api.php @@ -4,6 +4,7 @@ use App\Domains\CostUnit\Controllers\CreateController; use App\Domains\CostUnit\Controllers\DistanceAllowanceController; use App\Domains\CostUnit\Controllers\EditController; use App\Domains\CostUnit\Controllers\ExportController; +use App\Domains\CostUnit\Controllers\GlobalSepaExportController; use App\Domains\CostUnit\Controllers\ListController; use App\Domains\CostUnit\Controllers\OpenController; use App\Domains\CostUnit\Controllers\TreasurersEditController; @@ -43,6 +44,9 @@ Route::prefix('api/v1') + Route::get('/global-actions', [GlobalSepaExportController::class, 'getGlobalActions']); + Route::get('/export-sepa-file', [GlobalSepaExportController::class, 'exportSepaFile']); + Route::prefix('open')->group(function () { Route::get('/current-events', [ListController::class, 'listCurrentEvents']); Route::get('/current-running-jobs', [ListController::class, 'listCurrentRunningJobs']); diff --git a/app/Domains/CostUnit/Views/List.vue b/app/Domains/CostUnit/Views/List.vue index b842a97..b0b95d5 100644 --- a/app/Domains/CostUnit/Views/List.vue +++ b/app/Domains/CostUnit/Views/List.vue @@ -7,6 +7,7 @@ import TabbedPage from "../../../Views/Components/TabbedPage.vue"; import {toast} from "vue3-toastify"; import ListCostUnits from "./Partials/ListCostUnits.vue"; +import GlobalActions from "./Partials/GlobalActions.vue"; const props = defineProps({ message: String, @@ -63,6 +64,13 @@ const tabs = [ deep_jump_id: initialCostUnitId, deep_jump_id_sub: initialInvoiceId, }, + { + title: 'Globale Aktionen', + component: GlobalActions, + endpoint: "/api/v1/cost-unit/global-actions", + deep_jump_id: 0, + deep_jump_id_sub: 0, + }, ] onMounted(() => { diff --git a/app/Domains/CostUnit/Views/Partials/GlobalActions.vue b/app/Domains/CostUnit/Views/Partials/GlobalActions.vue new file mode 100644 index 0000000..89235a2 --- /dev/null +++ b/app/Domains/CostUnit/Views/Partials/GlobalActions.vue @@ -0,0 +1,112 @@ + + + + + diff --git a/app/Domains/Invoice/Actions/CreateInvoice/CreateInvoiceCommand.php b/app/Domains/Invoice/Actions/CreateInvoice/CreateInvoiceCommand.php index 8aeac1c..a922629 100644 --- a/app/Domains/Invoice/Actions/CreateInvoice/CreateInvoiceCommand.php +++ b/app/Domains/Invoice/Actions/CreateInvoice/CreateInvoiceCommand.php @@ -30,7 +30,7 @@ class CreateInvoiceCommand { 'type' => $this->request->invoiceType, 'type_other' => $this->request->invoiceTypeExtended, 'donation' => $this->request->isDonation, - 'user_id' => $this->request->userId, + 'user_id' => $this->request->paymentPurpose === null ? $this->request->userId : null, 'contact_name' => $this->request->contactName, 'contact_email' => $this->request->contactEmail, 'contact_phone' => $this->request->contactPhone, diff --git a/app/Domains/Legal/Controllers/GdprController.php b/app/Domains/Legal/Controllers/GdprController.php new file mode 100644 index 0000000..fb99cda --- /dev/null +++ b/app/Domains/Legal/Controllers/GdprController.php @@ -0,0 +1,20 @@ + 'Datenschutzerklärung', + 'content' => $this->tenant->gdpr_text ?? '', + ]); + return $inertiaProvider->render(); + } +} diff --git a/app/Domains/Legal/Controllers/ImpressController.php b/app/Domains/Legal/Controllers/ImpressController.php new file mode 100644 index 0000000..694a727 --- /dev/null +++ b/app/Domains/Legal/Controllers/ImpressController.php @@ -0,0 +1,20 @@ + 'Impressum', + 'content' => $this->tenant->impress_text ?? '', + ]); + return $inertiaProvider->render(); + } +} diff --git a/app/Domains/Legal/Routes/web.php b/app/Domains/Legal/Routes/web.php new file mode 100644 index 0000000..34ad7ee --- /dev/null +++ b/app/Domains/Legal/Routes/web.php @@ -0,0 +1,11 @@ +group(function () { + Route::get('/impress', ImpressController::class); + Route::get('/gdpr', GdprController::class); +}); diff --git a/app/Domains/Legal/Views/LegalPage.vue b/app/Domains/Legal/Views/LegalPage.vue new file mode 100644 index 0000000..73fa216 --- /dev/null +++ b/app/Domains/Legal/Views/LegalPage.vue @@ -0,0 +1,83 @@ + + + + + diff --git a/app/Domains/UserManagement/Controllers/LoginController.php b/app/Domains/UserManagement/Controllers/LoginController.php index b931ece..bee7779 100644 --- a/app/Domains/UserManagement/Controllers/LoginController.php +++ b/app/Domains/UserManagement/Controllers/LoginController.php @@ -2,6 +2,7 @@ namespace App\Domains\UserManagement\Controllers; +use App\Enumerations\UserRole; use App\Providers\InertiaProvider; use App\Scopes\CommonController; use Illuminate\Http\Request; @@ -38,20 +39,44 @@ class LoginController extends CommonController { return redirect()->intended('/register/verifyEmail'); } - - #$credentials = ['username' => 'development', 'password' => 'development']; - if (!Auth::attempt($credentials)) { return back()->withErrors([ 'username' => 'Diese Zugangsdaten sind ungültig.', ]); } - $request->session()->regenerate(); $user = Auth::user(); + $tenant = app('tenant'); + // Auf "lv" darf sich grundsätzlich jeder aktive Nutzer einloggen. + // Auf Sub-Tenants gilt: + // - Der Nutzer muss dem Tenant zugeordnet sein (local_group) + // - ODER er hat "Bundesrecht über Landesrecht": + // user_role_main === ROLE_ADMINISTRATOR -> Login auf jedem Sub-Tenant erlaubt. + $isMainAdmin = $user->user_role_main === UserRole::USER_ROLE_ADMIN; + $isMemberOfTenant = $tenant->slug === $user->local_group; -# dd($user->firstname . ' ' . $user->lastname); + if ($tenant->slug !== 'lv' && !$isMainAdmin && !$isMemberOfTenant) { + Auth::logout(); + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return back()->withErrors([ + 'username' => 'Diese Zugangsdaten sind für diesen Stamm nicht gültig.', + ]); + } + + if (!$user->active) { + Auth::logout(); + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return back()->withErrors([ + 'username' => 'Dieses Benutzerkonto ist nicht aktiv.', + ]); + } + + $request->session()->regenerate(); return redirect()->intended('/'); } diff --git a/app/Middleware/AdminRoleMiddleware.php b/app/Middleware/AdminRoleMiddleware.php new file mode 100644 index 0000000..c84a9a6 --- /dev/null +++ b/app/Middleware/AdminRoleMiddleware.php @@ -0,0 +1,26 @@ +check()) { + return redirect('/login')->with('message', 'Du musst eingeloggt sein.'); + } + + $authCheck = new AuthCheckProvider(); + $role = $authCheck->getUserRole(); + + if (!in_array($role, [UserRole::USER_ROLE_ADMIN, UserRole::USER_ROLE_GROUP_LEADER], true)) { + return redirect('/')->with('message', 'Du bist dazu nicht berechtigt.'); + } + + return $next($request); + } +} diff --git a/app/Middleware/LvOnlyMiddleware.php b/app/Middleware/LvOnlyMiddleware.php new file mode 100644 index 0000000..e368c07 --- /dev/null +++ b/app/Middleware/LvOnlyMiddleware.php @@ -0,0 +1,17 @@ +slug !== 'lv') { + return redirect('/admin')->with('message', 'Diese Funktion ist nur auf LV-Ebene verfügbar.'); + } + + return $next($request); + } +} diff --git a/app/Models/SepaPaymentElement.php b/app/Models/SepaPaymentElement.php new file mode 100644 index 0000000..e8761fe --- /dev/null +++ b/app/Models/SepaPaymentElement.php @@ -0,0 +1,54 @@ + 'boolean', + 'exported_at' => 'datetime', + ]; + + public function invoice(): BelongsTo + { + return $this->belongsTo(Invoice::class); + } + + public function costUnit(): BelongsTo + { + return $this->belongsTo(CostUnit::class); + } +} diff --git a/app/Providers/AuthCheckProvider.php b/app/Providers/AuthCheckProvider.php index c695f77..1209f95 100644 --- a/app/Providers/AuthCheckProvider.php +++ b/app/Providers/AuthCheckProvider.php @@ -3,6 +3,7 @@ namespace App\Providers; use App\Enumerations\UserRole; +use App\Models\User; class AuthCheckProvider { public function checkLoggedIn() : bool { @@ -16,7 +17,7 @@ class AuthCheckProvider { return $user->active; } - if ($user->user_role_main === UserRole::USER_ROLE_ADMIN) { + if ($this->isMainAdministrator($user)) { return true; } @@ -28,10 +29,39 @@ class AuthCheckProvider { return null; } + $user = auth()->user(); + if (app('tenant')->slug === 'lv') { - return auth()->user()->user_role_main; + return $user->user_role_main; } - return auth()->user()->user_role_local_group; + // "Bundesrecht steht über Landesrecht": + // Ein ROLE_ADMINISTRATOR auf LV-Ebene ist auf jedem Sub-Tenant automatisch Administrator, + // unabhängig von user_role_local_group. + if ($this->isMainAdministrator($user)) { + return UserRole::USER_ROLE_ADMIN; + } + + return $user->user_role_local_group; + } + + /** + * Gibt true zurück, wenn der Nutzer auf LV-Ebene Administrator ist. + * Diese Rolle hebt das lokale Rechtesystem für alle Sub-Tenants auf. + */ + public function isMainAdministrator(?User $user = null) : bool { + $user ??= auth()->user(); + + return $user !== null + && $user->user_role_main === UserRole::USER_ROLE_ADMIN; + } + + /** + * Bequemer Helper für die Berechtigungs-Checks im gesamten System. + * Gibt true zurück, wenn der aktuell eingeloggte Nutzer im Kontext des + * aktuellen Tenants effektiv Administrator ist. + */ + public function isAdministrator() : bool { + return $this->getUserRole() === UserRole::USER_ROLE_ADMIN; } } diff --git a/app/Providers/GlobalDataProvider.php b/app/Providers/GlobalDataProvider.php index df39737..5383949 100644 --- a/app/Providers/GlobalDataProvider.php +++ b/app/Providers/GlobalDataProvider.php @@ -20,34 +20,21 @@ class GlobalDataProvider { public function __invoke() { $this->user = auth()->user(); + $canAccessAdmin = false; + if (null !== $this->user) { + $authCheck = new AuthCheckProvider(); + $effectiveRole = $authCheck->getUserRole(); + $canAccessAdmin = in_array($effectiveRole, [UserRole::USER_ROLE_ADMIN, UserRole::USER_ROLE_GROUP_LEADER], true); + } + return response()->json([ 'user' => null !== $this->user ? new UserResource($this->user)->toArray(request()) : null, 'navbar' => $this->generateNavbar(), 'tenant' => app('tenant'), 'activeUsers' => $this->getActiveUsers(), 'version' => config('app.version'), - ]); - } - - public function getAllInvoiceTypes() : JsonResponse { - $invoiceTypes = []; - foreach (InvoiceType::orderBy('sort_order')->get() as $invoiceType) { - if ( - $invoiceType->slug === InvoiceType::INVOICE_TYPE_OTHER - ) { - continue; - } - - $invoiceTypes[] = [ - 'slug' => $invoiceType->slug, - 'name' => $invoiceType->name - ]; - } - - $invoiceTypes[] = ['slug' => InvoiceType::INVOICE_TYPE_OTHER, 'name' => 'Sonstige Kosten']; - - return response()->json([ - 'invoiceTypes' => $invoiceTypes + 'currentEvent' => $this->getCurrentEventData(), + 'canAccessAdmin' => $canAccessAdmin, ]); } @@ -99,10 +86,47 @@ class GlobalDataProvider { ]); } + private function getCurrentEventData() : ?array { + if (null === $this->user) { + return null; + } + + $currentEvent = new EventRepository()->getMyCurrentEvent(); + if (null === $currentEvent) { + return null; + } + + return [ + 'identifier' => $currentEvent->identifier, + 'name' => $currentEvent->name, + ]; + } + + public function getAllInvoiceTypes() : JsonResponse { + $invoiceTypes = []; + foreach (InvoiceType::orderBy('sort_order')->get() as $invoiceType) { + if ( + $invoiceType->slug === InvoiceType::INVOICE_TYPE_OTHER + ) { + continue; + } + + $invoiceTypes[] = [ + 'slug' => $invoiceType->slug, + 'name' => $invoiceType->name + ]; + } + + $invoiceTypes[] = ['slug' => InvoiceType::INVOICE_TYPE_OTHER, 'name' => 'Sonstige Kosten']; + + return response()->json([ + 'invoiceTypes' => $invoiceTypes + ]); + } + private function generateNavbar() : array { $eventRepository = new EventRepository(); - $navigation = [ 'personal' => [], 'common' => [], @@ -116,9 +140,11 @@ class GlobalDataProvider { $navigation['personal'][] = ['url' => '/personal-data', 'display' => 'Meine Daten']; $navigation['personal'][] = ['url' => '/messages', 'display' => 'Meine Nachrichten']; + $authCheck = new AuthCheckProvider(); + $effectiveRole = $authCheck->getUserRole(); + if ( - in_array($this->user->user_role_local_group, [UserRole::USER_ROLE_ADMIN, UserRole::USER_ROLE_GROUP_LEADER] ) || - $this->user->user_role_main === UserRole::USER_ROLE_ADMIN + in_array($effectiveRole, [UserRole::USER_ROLE_ADMIN, UserRole::USER_ROLE_GROUP_LEADER], true) ) { $navigation['costunits'][] = ['url' => '/cost-unit/list', 'display' => 'Kostenstellen']; $navigation['costunits'][] = ['url' => '/cost-unit/create', 'display' => 'Neue laufende Tätigkeit']; diff --git a/app/Providers/PainFileProvider.php b/app/Providers/PainFileProvider.php index 567b978..32b51e2 100644 --- a/app/Providers/PainFileProvider.php +++ b/app/Providers/PainFileProvider.php @@ -2,25 +2,23 @@ namespace App\Providers; -use App\Models\Invoice; -use App\Resources\InvoiceResource; +use App\Models\SepaPaymentElement; use DOMDocument; use Exception; -use Illuminate\Http\Request; class PainFileProvider { public string $senderIban; public string $senderName; public string $senderBic; - /* @var Invoice[] */ - public array $invoices; + /** @var SepaPaymentElement[] */ + public array $elements; - public function __construct(string $senderIban, string $senderName, string $senderBic, array $invoices) { + public function __construct(string $senderIban, string $senderName, string $senderBic, array $elements) { $this->senderIban = $senderIban; $this->senderName = $senderName; $this->senderBic = $senderBic; - $this->invoices = $invoices; + $this->elements = $elements; } public function createPainFileContent() : string { @@ -46,9 +44,9 @@ class PainFileProvider { $grp_hdr->appendChild($doc->createElement('MsgId', uniqid('MSG'))); $grp_hdr->appendChild($doc->createElement('CreDtTm', date('c'))); - $grp_hdr->appendChild($doc->createElement('NbOfTxs', count($this->invoices))); + $grp_hdr->appendChild($doc->createElement('NbOfTxs', count($this->elements))); - $totalAmount = array_sum(array_column($this->invoices, 'amount')); + $totalAmount = array_sum(array_map(fn(SepaPaymentElement $e) => $e->amount, $this->elements)); $grp_hdr->appendChild($doc->createElement('CtrlSum', number_format($totalAmount, 2, '.', ''))); $initg_pty = $doc->createElement('InitgPty'); @@ -62,7 +60,7 @@ class PainFileProvider { $pmt_inf->appendChild($doc->createElement('PmtInfId', uniqid('PMT'))); $pmt_inf->appendChild($doc->createElement('PmtMtd', 'TRF')); $pmt_inf->appendChild($doc->createElement('BtchBookg', 'false')); - $pmt_inf->appendChild($doc->createElement('NbOfTxs', count($this->invoices))); + $pmt_inf->appendChild($doc->createElement('NbOfTxs', count($this->elements))); $pmt_inf->appendChild($doc->createElement('CtrlSum', number_format($totalAmount, 2, '.', ''))); $pmt_tp_inf = $doc->createElement('PmtTpInf'); @@ -90,9 +88,7 @@ class PainFileProvider { $dbtr_agt->appendChild($id); $pmt_inf->appendChild($dbtr_agt); - foreach ($this->invoices as $index => $invoice) { - $invoiceResource = new InvoiceResource($invoice)->toArray(new Request()); - + foreach ($this->elements as $index => $element) { $cdt_trf_tx_inf = $doc->createElement('CdtTrfTxInf'); $pmt_id = $doc->createElement('PmtId'); @@ -100,23 +96,23 @@ class PainFileProvider { $cdt_trf_tx_inf->appendChild($pmt_id); $amt = $doc->createElement('Amt'); - $instd_amt = $doc->createElement('InstdAmt', number_format($invoice['amount'], 2, '.', '')); + $instd_amt = $doc->createElement('InstdAmt', number_format($element->amount, 2, '.', '')); $instd_amt->setAttribute('Ccy', 'EUR'); $amt->appendChild($instd_amt); $cdt_trf_tx_inf->appendChild($amt); $cdtr = $doc->createElement('Cdtr'); - $cdtr->appendChild($doc->createElement('Nm', $invoice['contact_bank_owner'])); + $cdtr->appendChild($doc->createElement('Nm', $element->recipient_name)); $cdt_trf_tx_inf->appendChild($cdtr); $cdtr_acct = $doc->createElement('CdtrAcct'); $cdtr_id = $doc->createElement('Id'); - $cdtr_id->appendChild($doc->createElement('IBAN', str_replace(' ', '', $invoice['contact_bank_iban']))); + $cdtr_id->appendChild($doc->createElement('IBAN', str_replace(' ', '', $element->recipient_iban))); $cdtr_acct->appendChild($cdtr_id); $cdt_trf_tx_inf->appendChild($cdtr_acct); $rmt_inf = $doc->createElement('RmtInf'); - $rmt_inf->appendChild($doc->createElement('Ustrd', $invoiceResource['paymentPurpose'])); + $rmt_inf->appendChild($doc->createElement('Ustrd', $element->payment_purpose)); $cdt_trf_tx_inf->appendChild($rmt_inf); $pmt_inf->appendChild($cdt_trf_tx_inf); diff --git a/app/Providers/TenantUserProvider.php b/app/Providers/TenantUserProvider.php index 909769c..5d01b77 100644 --- a/app/Providers/TenantUserProvider.php +++ b/app/Providers/TenantUserProvider.php @@ -2,6 +2,7 @@ namespace App\Providers; +use App\Enumerations\UserRole; use Illuminate\Auth\EloquentUserProvider; class TenantUserProvider extends EloquentUserProvider @@ -18,15 +19,20 @@ class TenantUserProvider extends EloquentUserProvider } } + // Auf "lv" gilt grundsätzlich keine local_group-Einschränkung. if (app('tenant')->slug === 'lv') { return $query->first(); } - $query->where([ - 'local_group' => app('tenant')->slug, - 'active' => true - - ]); + // Auf Sub-Tenants: + // - Entweder gehört der Nutzer zum aktuellen Tenant (local_group) + // - ODER er ist auf LV-Ebene Administrator + // -> "Bundesrecht steht über Landesrecht": Login überall möglich. + $query->where('active', true) + ->where(function ($q) { + $q->where('local_group', app('tenant')->slug) + ->orWhere('user_role_main', UserRole::USER_ROLE_ADMIN); + }); return $query->first(); } diff --git a/app/Repositories/CostUnitRepository.php b/app/Repositories/CostUnitRepository.php index a976501..37c7f86 100644 --- a/app/Repositories/CostUnitRepository.php +++ b/app/Repositories/CostUnitRepository.php @@ -7,6 +7,7 @@ use App\Enumerations\InvoiceStatus; use App\Enumerations\InvoiceType; use App\Enumerations\UserRole; use App\Models\CostUnit; +use App\Providers\AuthCheckProvider; use App\Resources\CostUnitResource; use App\ValueObjects\Amount; use Illuminate\Database\Capsule\Manager as Capsule; @@ -75,8 +76,8 @@ class CostUnitRepository { } else { if ($tenant->slug !== 'lv') { if ( - $user->user_role_main === UserRole::USER_ROLE_ADMIN || - in_array($user->user_role_local_group, [UserRole::USER_ROLE_GROUP_LEADER, UserRole::USER_ROLE_ADMIN]) + new AuthCheckProvider()->isAdministrator() || + $user->user_role_local_group === UserRole::USER_ROLE_ADMIN ) { $canSeeAll = true; } diff --git a/app/Repositories/EventRepository.php b/app/Repositories/EventRepository.php index 5f38447..b4eafef 100644 --- a/app/Repositories/EventRepository.php +++ b/app/Repositories/EventRepository.php @@ -6,6 +6,7 @@ use App\Enumerations\ParticipationType; use App\Enumerations\UserRole; use App\Models\CostUnit; use App\Models\Event; +use App\Providers\AuthCheckProvider; use App\Resources\CostUnitResource; use Illuminate\Database\Eloquent\Collection; use Illuminate\Http\Request; @@ -19,13 +20,25 @@ class EventRepository { } + public function getMyCurrentEvent() : ?Event { + $events = $this->getEventsByCriteria([ + ['archived', '=', false], + ['start_date', '<=', now()], + ['end_date', '>=', now()], + ], true); + if (count($events) !== 1) { + return null; + } + return $events[0]; + } + public function getUpcoming(int $maxCount = 5, bool $accessCheck = true) : array { $events = []; foreach ( $this->getEventsByCriteria([ 'archived' => false, ],$accessCheck) as $event) { - if ($event->start_date > now()) { + if ($event->end_date >= now()) { $event = $event->toResource()->toArray(new Request()); $events[] = $event; @@ -72,7 +85,10 @@ class EventRepository { if (!$accessCheck) { $canSeeAll = true; } else { - if ($tenant->slug !== 'lv') { + if ( + new AuthCheckProvider()->isAdministrator() || + $user->user_role_local_group === UserRole::USER_ROLE_ADMIN + ) { if ( $user->user_role_main === UserRole::USER_ROLE_ADMIN || in_array($user->user_role_local_group, [UserRole::USER_ROLE_GROUP_LEADER, UserRole::USER_ROLE_ADMIN]) diff --git a/app/Scopes/CommonController.php b/app/Scopes/CommonController.php index dd0efae..695b531 100644 --- a/app/Scopes/CommonController.php +++ b/app/Scopes/CommonController.php @@ -2,6 +2,8 @@ namespace App\Scopes; +use App\Domains\Admin\Repositories\AdminTenantRepository; +use App\Domains\Admin\Repositories\AdminUserRepository; use App\Models\Tenant; use App\Providers\AuthCheckProvider; use App\Repositories\CostUnitRepository; @@ -23,6 +25,8 @@ abstract class CommonController { protected EventRepository $events; protected EventParticipantRepository $eventParticipants; protected EstimatesRepository $estimates; + protected AdminUserRepository $adminUsers; + protected AdminTenantRepository $adminTenants; public function __construct() { $this->tenant = app('tenant'); @@ -33,6 +37,8 @@ abstract class CommonController { $this->events = new EventRepository(); $this->eventParticipants = new EventParticipantRepository(); $this->estimates = new EstimatesRepository(); + $this->adminUsers = new AdminUserRepository(); + $this->adminTenants = new AdminTenantRepository(); } protected function checkAuth() { diff --git a/database/migrations/2026_06_21_000001_alter_tenants_gdpr_impress_to_longtext.php b/database/migrations/2026_06_21_000001_alter_tenants_gdpr_impress_to_longtext.php new file mode 100644 index 0000000..5f26e73 --- /dev/null +++ b/database/migrations/2026_06_21_000001_alter_tenants_gdpr_impress_to_longtext.php @@ -0,0 +1,23 @@ +longText('gdpr_text')->nullable()->change(); + $table->longText('impress_text')->nullable()->change(); + }); + } + + public function down(): void + { + Schema::table('tenants', function (Blueprint $table) { + $table->string('gdpr_text')->nullable()->change(); + $table->string('impress_text')->nullable()->change(); + }); + } +}; diff --git a/database/migrations/2026_06_21_140010_create_sepa_payment_elements.php b/database/migrations/2026_06_21_140010_create_sepa_payment_elements.php new file mode 100644 index 0000000..4bb8f52 --- /dev/null +++ b/database/migrations/2026_06_21_140010_create_sepa_payment_elements.php @@ -0,0 +1,31 @@ +uuid('id')->primary(); + $table->string('tenant'); + $table->foreignId('invoice_id')->constrained('invoices', 'id')->restrictOnDelete()->cascadeOnUpdate(); + $table->foreignId('cost_unit_id')->constrained('cost_units', 'id')->restrictOnDelete()->cascadeOnUpdate(); + $table->float('amount', 2); + $table->string('recipient_name'); + $table->string('recipient_iban'); + $table->string('payment_purpose'); + $table->boolean('exported')->default(false); + $table->dateTime('exported_at')->nullable(); + + $table->foreign('tenant')->references('slug')->on('tenants')->restrictOnDelete()->cascadeOnUpdate(); + + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('sepa_payment_elements'); + } +}; diff --git a/public/css/app.css b/public/css/app.css index 5194325..acc89c6 100644 --- a/public/css/app.css +++ b/public/css/app.css @@ -57,8 +57,8 @@ h1, h2, h3, h4, h5, h6 { .header .user-info { position: relative; - right: calc(-100% + 190px); - width: 195px; + right: calc(-100% + 250px); + width: 240px; overflow: hidden; border-radius: 50px 0 0 50px; text-align: right; @@ -108,11 +108,11 @@ h1, h2, h3, h4, h5, h6 { display: flex; align-items: center; justify-content: center; + height: 180px; } .logo img { - width: 135px !important; - height: 70px !important; + width: 100% !important; } .footer { diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 0000000..149bbee Binary files /dev/null and b/public/favicon.png differ diff --git a/public/images/mareike.png b/public/images/mareike.png new file mode 100644 index 0000000..149bbee Binary files /dev/null and b/public/images/mareike.png differ diff --git a/resources/js/layouts/AdminAppLayout.vue b/resources/js/layouts/AdminAppLayout.vue new file mode 100644 index 0000000..f0e4370 --- /dev/null +++ b/resources/js/layouts/AdminAppLayout.vue @@ -0,0 +1,459 @@ + + + + + diff --git a/resources/js/layouts/AppLayout.vue b/resources/js/layouts/AppLayout.vue index f8c1aa4..4fffb30 100644 --- a/resources/js/layouts/AppLayout.vue +++ b/resources/js/layouts/AppLayout.vue @@ -1,5 +1,5 @@