From cfc7c7eee2bbc83120375cd5f14ee6e7ff50cab0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20G=C3=BCnther?=
Date: Sun, 21 Jun 2026 20:46:16 +0200
Subject: [PATCH] Tenant management
---
.../ManagedTenantContactGetController.php | 24 ++
.../ManagedTenantContactUpdateController.php | 28 ++
.../ManagedTenantGdprGetController.php | 21 ++
.../ManagedTenantGdprUpdateController.php | 25 ++
.../ManagedTenantImpressGetController.php | 21 ++
.../ManagedTenantImpressUpdateController.php | 25 ++
.../ManagedTenantPaymentGetController.php | 23 ++
.../ManagedTenantPaymentUpdateController.php | 27 ++
.../TenantContactGetController.php | 1 +
.../Controllers/TenantCreateController.php | 35 +++
.../Controllers/TenantEditPageController.php | 27 ++
.../Controllers/TenantGdprGetController.php | 1 +
.../TenantGeneralGetController.php | 24 ++
.../TenantGeneralUpdateController.php | 27 ++
.../TenantImpressGetController.php | 1 +
.../Controllers/TenantListApiController.php | 25 ++
.../Controllers/TenantListPageController.php | 17 ++
.../TenantPaymentGetController.php | 1 +
app/Domains/Admin/Routes/api.php | 40 ++-
app/Domains/Admin/Routes/web.php | 8 +
.../Admin/Views/Partials/TenantContact.vue | 3 +-
.../Admin/Views/Partials/TenantGdpr.vue | 3 +-
.../Admin/Views/Partials/TenantGeneral.vue | 157 +++++++++++
.../Admin/Views/Partials/TenantImpress.vue | 3 +-
.../Admin/Views/Partials/TenantPayment.vue | 3 +-
app/Domains/Admin/Views/TenantEdit.vue | 52 ++++
app/Domains/Admin/Views/TenantList.vue | 248 ++++++++++++++++++
app/Middleware/LvOnlyMiddleware.php | 17 ++
resources/js/layouts/AdminAppLayout.vue | 5 +
29 files changed, 884 insertions(+), 8 deletions(-)
create mode 100644 app/Domains/Admin/Controllers/ManagedTenantContactGetController.php
create mode 100644 app/Domains/Admin/Controllers/ManagedTenantContactUpdateController.php
create mode 100644 app/Domains/Admin/Controllers/ManagedTenantGdprGetController.php
create mode 100644 app/Domains/Admin/Controllers/ManagedTenantGdprUpdateController.php
create mode 100644 app/Domains/Admin/Controllers/ManagedTenantImpressGetController.php
create mode 100644 app/Domains/Admin/Controllers/ManagedTenantImpressUpdateController.php
create mode 100644 app/Domains/Admin/Controllers/ManagedTenantPaymentGetController.php
create mode 100644 app/Domains/Admin/Controllers/ManagedTenantPaymentUpdateController.php
create mode 100644 app/Domains/Admin/Controllers/TenantCreateController.php
create mode 100644 app/Domains/Admin/Controllers/TenantEditPageController.php
create mode 100644 app/Domains/Admin/Controllers/TenantGeneralGetController.php
create mode 100644 app/Domains/Admin/Controllers/TenantGeneralUpdateController.php
create mode 100644 app/Domains/Admin/Controllers/TenantListApiController.php
create mode 100644 app/Domains/Admin/Controllers/TenantListPageController.php
create mode 100644 app/Domains/Admin/Views/Partials/TenantGeneral.vue
create mode 100644 app/Domains/Admin/Views/TenantEdit.vue
create mode 100644 app/Domains/Admin/Views/TenantList.vue
create mode 100644 app/Middleware/LvOnlyMiddleware.php
diff --git a/app/Domains/Admin/Controllers/ManagedTenantContactGetController.php b/app/Domains/Admin/Controllers/ManagedTenantContactGetController.php
new file mode 100644
index 0000000..90dcbf8
--- /dev/null
+++ b/app/Domains/Admin/Controllers/ManagedTenantContactGetController.php
@@ -0,0 +1,24 @@
+firstOrFail();
+
+ 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..a102aac
--- /dev/null
+++ b/app/Domains/Admin/Controllers/ManagedTenantContactUpdateController.php
@@ -0,0 +1,28 @@
+firstOrFail();
+
+ $tenant->update([
+ 'email' => $request->input('email'),
+ 'email_finance' => $request->input('email_finance'),
+ 'postcode' => $request->input('postcode'),
+ 'city' => $request->input('city'),
+ ]);
+
+ return response()->json([
+ 'status' => 'success',
+ 'message' => 'Kontaktdaten wurden gespeichert.',
+ ]);
+ }
+}
diff --git a/app/Domains/Admin/Controllers/ManagedTenantGdprGetController.php b/app/Domains/Admin/Controllers/ManagedTenantGdprGetController.php
new file mode 100644
index 0000000..6cc472d
--- /dev/null
+++ b/app/Domains/Admin/Controllers/ManagedTenantGdprGetController.php
@@ -0,0 +1,21 @@
+firstOrFail();
+
+ 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..96a3826
--- /dev/null
+++ b/app/Domains/Admin/Controllers/ManagedTenantGdprUpdateController.php
@@ -0,0 +1,25 @@
+firstOrFail();
+
+ $tenant->update([
+ 'gdpr_text' => $request->input('gdpr_text'),
+ ]);
+
+ return response()->json([
+ 'status' => 'success',
+ 'message' => 'Datenschutzerklärung wurde gespeichert.',
+ ]);
+ }
+}
diff --git a/app/Domains/Admin/Controllers/ManagedTenantImpressGetController.php b/app/Domains/Admin/Controllers/ManagedTenantImpressGetController.php
new file mode 100644
index 0000000..dd188a8
--- /dev/null
+++ b/app/Domains/Admin/Controllers/ManagedTenantImpressGetController.php
@@ -0,0 +1,21 @@
+firstOrFail();
+
+ 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..09e23f1
--- /dev/null
+++ b/app/Domains/Admin/Controllers/ManagedTenantImpressUpdateController.php
@@ -0,0 +1,25 @@
+firstOrFail();
+
+ $tenant->update([
+ 'impress_text' => $request->input('impress_text'),
+ ]);
+
+ return response()->json([
+ 'status' => 'success',
+ 'message' => 'Impressum wurde gespeichert.',
+ ]);
+ }
+}
diff --git a/app/Domains/Admin/Controllers/ManagedTenantPaymentGetController.php b/app/Domains/Admin/Controllers/ManagedTenantPaymentGetController.php
new file mode 100644
index 0000000..5d8e258
--- /dev/null
+++ b/app/Domains/Admin/Controllers/ManagedTenantPaymentGetController.php
@@ -0,0 +1,23 @@
+firstOrFail();
+
+ 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..e01d39a
--- /dev/null
+++ b/app/Domains/Admin/Controllers/ManagedTenantPaymentUpdateController.php
@@ -0,0 +1,27 @@
+firstOrFail();
+
+ $tenant->update([
+ 'account_iban' => $request->input('account_iban'),
+ 'account_bic' => $request->input('account_bic'),
+ 'account_name' => $request->input('account_name'),
+ ]);
+
+ return response()->json([
+ 'status' => 'success',
+ 'message' => 'Bezahldaten wurden gespeichert.',
+ ]);
+ }
+}
diff --git a/app/Domains/Admin/Controllers/TenantContactGetController.php b/app/Domains/Admin/Controllers/TenantContactGetController.php
index f69f58e..b660d45 100644
--- a/app/Domains/Admin/Controllers/TenantContactGetController.php
+++ b/app/Domains/Admin/Controllers/TenantContactGetController.php
@@ -15,6 +15,7 @@ class TenantContactGetController extends CommonController
'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/TenantCreateController.php b/app/Domains/Admin/Controllers/TenantCreateController.php
new file mode 100644
index 0000000..c92e269
--- /dev/null
+++ b/app/Domains/Admin/Controllers/TenantCreateController.php
@@ -0,0 +1,35 @@
+ $request->input('name'),
+ 'slug' => $request->input('slug'),
+ 'url' => $request->input('url'),
+ 'email' => '',
+ 'email_finance' => '',
+ 'account_name' => '',
+ 'account_iban' => '',
+ 'account_bic' => '',
+ 'city' => '',
+ 'postcode' => '',
+ 'is_active_local_group' => true,
+ 'has_active_instance' => true,
+ ]);
+
+ return response()->json([
+ 'status' => 'success',
+ 'message' => 'Stamm wurde angelegt.',
+ 'slug' => $tenant->slug,
+ ]);
+ }
+}
diff --git a/app/Domains/Admin/Controllers/TenantEditPageController.php b/app/Domains/Admin/Controllers/TenantEditPageController.php
new file mode 100644
index 0000000..dbf22a4
--- /dev/null
+++ b/app/Domains/Admin/Controllers/TenantEditPageController.php
@@ -0,0 +1,27 @@
+firstOrFail();
+
+ $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
index c0017b1..d98f35f 100644
--- a/app/Domains/Admin/Controllers/TenantGdprGetController.php
+++ b/app/Domains/Admin/Controllers/TenantGdprGetController.php
@@ -12,6 +12,7 @@ class TenantGdprGetController extends CommonController
{
return response()->json([
'gdpr_text' => $this->tenant->gdpr_text ?? '',
+ 'saveEndpoint' => '/api/v1/admin/tenant/gdpr',
]);
}
}
diff --git a/app/Domains/Admin/Controllers/TenantGeneralGetController.php b/app/Domains/Admin/Controllers/TenantGeneralGetController.php
new file mode 100644
index 0000000..0c3c877
--- /dev/null
+++ b/app/Domains/Admin/Controllers/TenantGeneralGetController.php
@@ -0,0 +1,24 @@
+firstOrFail();
+
+ 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..3f6277e
--- /dev/null
+++ b/app/Domains/Admin/Controllers/TenantGeneralUpdateController.php
@@ -0,0 +1,27 @@
+firstOrFail();
+
+ $tenant->update([
+ 'name' => $request->input('name'),
+ 'slug' => $request->input('slug'),
+ 'url' => $request->input('url'),
+ ]);
+
+ return response()->json([
+ 'status' => 'success',
+ 'message' => 'Allgemeine Daten wurden gespeichert.',
+ ]);
+ }
+}
diff --git a/app/Domains/Admin/Controllers/TenantImpressGetController.php b/app/Domains/Admin/Controllers/TenantImpressGetController.php
index e18b835..6419db4 100644
--- a/app/Domains/Admin/Controllers/TenantImpressGetController.php
+++ b/app/Domains/Admin/Controllers/TenantImpressGetController.php
@@ -12,6 +12,7 @@ class TenantImpressGetController extends CommonController
{
return response()->json([
'impress_text' => $this->tenant->impress_text ?? '',
+ 'saveEndpoint' => '/api/v1/admin/tenant/impress',
]);
}
}
diff --git a/app/Domains/Admin/Controllers/TenantListApiController.php b/app/Domains/Admin/Controllers/TenantListApiController.php
new file mode 100644
index 0000000..12c68e8
--- /dev/null
+++ b/app/Domains/Admin/Controllers/TenantListApiController.php
@@ -0,0 +1,25 @@
+get()->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' => $tenants]);
+ }
+}
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/TenantPaymentGetController.php b/app/Domains/Admin/Controllers/TenantPaymentGetController.php
index 0f38df1..1fc3a86 100644
--- a/app/Domains/Admin/Controllers/TenantPaymentGetController.php
+++ b/app/Domains/Admin/Controllers/TenantPaymentGetController.php
@@ -14,6 +14,7 @@ class TenantPaymentGetController extends CommonController
'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/Routes/api.php b/app/Domains/Admin/Routes/api.php
index 81a4241..d31a421 100644
--- a/app/Domains/Admin/Routes/api.php
+++ b/app/Domains/Admin/Routes/api.php
@@ -1,15 +1,28 @@
group(function () {
@@ -23,4 +36,23 @@ Route::middleware([IdentifyTenant::class, 'auth', AdminRoleMiddleware::class])->
Route::get('/gdpr', TenantGdprGetController::class);
Route::post('/gdpr', TenantGdprUpdateController::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
index 2025169..9846e40 100644
--- a/app/Domains/Admin/Routes/web.php
+++ b/app/Domains/Admin/Routes/web.php
@@ -1,14 +1,22 @@
group(function () {
Route::prefix('admin')->group(function () {
Route::get('/', AdminDashboardController::class);
Route::get('/tenant', TenantPageController::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/Partials/TenantContact.vue b/app/Domains/Admin/Views/Partials/TenantContact.vue
index d6bf63a..11843a5 100644
--- a/app/Domains/Admin/Views/Partials/TenantContact.vue
+++ b/app/Domains/Admin/Views/Partials/TenantContact.vue
@@ -21,7 +21,8 @@ const form = ref({
})
async function save() {
- const response = await request('/api/v1/admin/tenant/contact', {
+ const saveUrl = props.data.saveEndpoint ?? '/api/v1/admin/tenant/contact'
+ const response = await request(saveUrl, {
method: 'POST',
body: form.value,
})
diff --git a/app/Domains/Admin/Views/Partials/TenantGdpr.vue b/app/Domains/Admin/Views/Partials/TenantGdpr.vue
index 2fdf97a..bf4cb91 100644
--- a/app/Domains/Admin/Views/Partials/TenantGdpr.vue
+++ b/app/Domains/Admin/Views/Partials/TenantGdpr.vue
@@ -27,7 +27,8 @@ function autoGenerate() {
}
async function save() {
- const response = await request('/api/v1/admin/tenant/gdpr', {
+ const saveUrl = props.data.saveEndpoint ?? '/api/v1/admin/tenant/gdpr'
+ const response = await request(saveUrl, {
method: 'POST',
body: { gdpr_text: content.value },
})
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 @@
+
+
+
+
+
+ | Name: | {{ form.name }} |
+ | Slug: | {{ form.slug }} |
+ | URL: | {{ form.url }} |
+ | Status: |
+ Aktiv
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/Domains/Admin/Views/Partials/TenantImpress.vue b/app/Domains/Admin/Views/Partials/TenantImpress.vue
index 628d311..89f0799 100644
--- a/app/Domains/Admin/Views/Partials/TenantImpress.vue
+++ b/app/Domains/Admin/Views/Partials/TenantImpress.vue
@@ -16,7 +16,8 @@ const { request } = useAjax()
const content = ref(props.data.impress_text ?? '')
async function save() {
- const response = await request('/api/v1/admin/tenant/impress', {
+ const saveUrl = props.data.saveEndpoint ?? '/api/v1/admin/tenant/impress'
+ const response = await request(saveUrl, {
method: 'POST',
body: { impress_text: content.value },
})
diff --git a/app/Domains/Admin/Views/Partials/TenantPayment.vue b/app/Domains/Admin/Views/Partials/TenantPayment.vue
index 0b385ae..82123f6 100644
--- a/app/Domains/Admin/Views/Partials/TenantPayment.vue
+++ b/app/Domains/Admin/Views/Partials/TenantPayment.vue
@@ -20,7 +20,8 @@ const form = ref({
})
async function save() {
- const response = await request('/api/v1/admin/tenant/payment', {
+ const saveUrl = props.data.saveEndpoint ?? '/api/v1/admin/tenant/payment'
+ const response = await request(saveUrl, {
method: 'POST',
body: form.value,
})
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 @@
+
+
+
+
+
+
+
+
+ | Name |
+ Slug |
+ URL |
+ Status |
+
+
+
+
+ | {{ tenant.name }} |
+ {{ tenant.slug }} |
+ {{ tenant.url }} |
+
+
+ {{ tenant.is_active_local_group ? 'Aktiv' : 'Inaktiv' }}
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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/resources/js/layouts/AdminAppLayout.vue b/resources/js/layouts/AdminAppLayout.vue
index efa48a9..b4a1235 100644
--- a/resources/js/layouts/AdminAppLayout.vue
+++ b/resources/js/layouts/AdminAppLayout.vue
@@ -105,6 +105,11 @@ const props = defineProps({
+