diff --git a/saas-app/public/app-support.php b/saas-app/public/app-support.php index cce7947..6fb3c28 100644 --- a/saas-app/public/app-support.php +++ b/saas-app/public/app-support.php @@ -283,6 +283,196 @@ function app_can_manage_tenant(?array $auth): bool return app_is_platform_admin($auth) || app_is_tenant_admin($auth); } +function app_has_role(?array $auth, string $role): bool +{ + if (!is_array($auth)) { + return false; + } + + return in_array($role, $auth['roles'] ?? [], true); +} + +function app_can_manage_finance(?array $auth): bool +{ + return app_can_manage_tenant($auth) || app_has_role($auth, 'finance_admin'); +} + +function app_can_manage_support(?array $auth): bool +{ + return app_can_manage_tenant($auth) || app_has_role($auth, 'support_contact'); +} + +function app_can_manage_surveys(?array $auth): bool +{ + return app_can_manage_tenant($auth) || app_has_role($auth, 'survey_manager'); +} + +/** + * @return array + */ +function app_auth_role_labels(?array $auth): array +{ + if (!is_array($auth)) { + return []; + } + + if (app_is_platform_admin($auth) && empty($auth['acting_as_platform_admin'])) { + return ['Global-Admin']; + } + + $labels = []; + $map = [ + 'tenant_admin' => 'Tenant-Admin', + 'finance_admin' => 'Finanzen', + 'support_contact' => 'Support', + 'survey_manager' => 'Umfragen', + ]; + + foreach ($map as $role => $label) { + if (app_has_role($auth, $role)) { + $labels[] = $label; + } + } + + if ($labels === []) { + $labels[] = 'Mitglied'; + } + + if (!empty($auth['acting_as_platform_admin'])) { + array_unshift($labels, 'Global-Admin'); + } + + return array_values(array_unique($labels)); +} + +function app_primary_role_label(?array $auth): string +{ + $labels = app_auth_role_labels($auth); + + return $labels[0] ?? 'Mitglied'; +} + +/** + * @return array + */ +function app_tenant_nav_items(?array $auth, array $features = []): array +{ + if (!is_array($auth)) { + return [ + ['href' => '/', 'label' => 'Start'], + ['href' => '/login/', 'label' => 'Anmeldung'], + ['href' => '/admin/login/', 'label' => 'Betreiber'], + ]; + } + + $items = [ + ['href' => '/dashboard/', 'label' => 'Dashboard'], + ['href' => '/content/', 'label' => 'Hinweise'], + ['href' => '/support/', 'label' => 'Support'], + ['href' => '/surveys/', 'label' => 'Umfragen'], + ]; + + if (app_can_manage_tenant($auth)) { + $items[] = ['href' => '/members/', 'label' => 'Mitglieder']; + } + + if (app_can_manage_finance($auth)) { + $items[] = ['href' => '/ledger/', 'label' => 'Buchungen']; + $items[] = ['href' => '/payments/', 'label' => 'Einzahlungen']; + } + + if (app_can_manage_tenant($auth)) { + $items[] = ['href' => '/tenants/roles/', 'label' => 'Rollen']; + } + + if (app_can_manage_tenant($auth) && !empty($features['tenant_settings'])) { + $items[] = ['href' => '/settings/', 'label' => 'Einstellungen']; + } + + if ( + app_can_manage_tenant($auth) + && ( + !empty($features['pdf_export']) + || !empty($features['paper_strike_entry']) + || !empty($features['basic_exports']) + ) + ) { + $items[] = ['href' => '/exports/', 'label' => 'Exporte']; + } + + return $items; +} + +/** + * @param array $roles + */ +function app_auth_has_any_role(?array $auth, array $roles): bool +{ + if (!is_array($auth)) { + return false; + } + + $granted = $auth['roles'] ?? []; + + if (!is_array($granted)) { + return false; + } + + foreach ($roles as $role) { + if (in_array($role, $granted, true)) { + return true; + } + } + + return false; +} + +/** + * @return array + */ +function app_tenant_navigation_items(?array $auth, array $license = []): array +{ + if (!is_array($auth)) { + return [ + ['key' => 'home', 'label' => 'Start', 'href' => '/'], + ['key' => 'login', 'label' => 'Anmeldung', 'href' => '/login/'], + ['key' => 'admin', 'label' => 'Verwaltung', 'href' => '/admin/login/'], + ]; + } + + $features = is_array($license['features'] ?? null) ? $license['features'] : []; + $canManage = app_can_manage_tenant($auth); + $canFinance = $canManage || app_auth_has_any_role($auth, ['finance_admin']); + $hasExports = !empty($features['pdf_export']) || !empty($features['paper_strike_entry']) || !empty($features['basic_exports']); + + $items = [ + ['key' => 'dashboard', 'label' => 'Uebersicht', 'href' => '/dashboard/'], + ['key' => 'content', 'label' => 'Hinweise', 'href' => '/content/'], + ['key' => 'support', 'label' => 'Support', 'href' => '/support/'], + ['key' => 'surveys', 'label' => 'Umfragen', 'href' => '/surveys/'], + ]; + + if ($canFinance) { + $items[] = ['key' => 'ledger', 'label' => 'Buchungen', 'href' => '/ledger/']; + $items[] = ['key' => 'payments', 'label' => 'Zahlungen', 'href' => '/payments/']; + } + + if ($canManage) { + $items[] = ['key' => 'members', 'label' => 'Mitglieder', 'href' => '/members/']; + $items[] = ['key' => 'roles', 'label' => 'Rollen', 'href' => '/tenants/roles/']; + + if (!empty($features['tenant_settings'])) { + $items[] = ['key' => 'settings', 'label' => 'Einstellungen', 'href' => '/settings/']; + } + + if ($hasExports) { + $items[] = ['key' => 'exports', 'label' => 'Exporte', 'href' => '/exports/']; + } + } + + return $items; +} + function app_platform_admin_by_email(PDO $pdo, string $email): ?array { return app_query_one( diff --git a/saas-app/public/index.php b/saas-app/public/index.php index c5b6c24..451ad85 100644 --- a/saas-app/public/index.php +++ b/saas-app/public/index.php @@ -216,6 +216,7 @@ if ($auth !== null && in_array($page, $restrictedPages, true) && !app_can_manage } $canManageTenant = app_can_manage_tenant($auth); +$tenantNavItems = app_tenant_navigation_items($auth, $tenantLicense); ?> @@ -224,8 +225,8 @@ $canManageTenant = app_can_manage_tenant($auth); Kaffeeliste SaaS @@ -233,25 +234,17 @@ $canManageTenant = app_can_manage_tenant($auth);
Kaffeeliste SaaS - Zentrale Anmeldung, Tenant-Verwaltung und alle Kernfunktionen der Kaffeeliste in einer Oberfläche. + Kaffeeliste, Hinweise und Support in einem Menü.
@@ -319,7 +312,7 @@ $canManageTenant = app_can_manage_tenant($auth);
Zentrale Anmeldung
-

Mitglieder melden sich zentral mit ihrer E-Mail-Adresse an.

+

Anmeldung

Zuerst wird der passende Bereich ermittelt. Falls mehrere Zuordnungen vorhanden sind, wählst du den richtigen Tenant aus und gibst danach dein Passwort ein.

@@ -387,7 +380,7 @@ $canManageTenant = app_can_manage_tenant($auth);
Betreiber-Sicht
-

Die Tenant-Übersicht bündelt Portfolio, Zugänge und Betriebsstatus an einem Ort.

+

Mandanten

Hier sehen Betreiber, wie viele Tenants aktiv sind, wie die Anmeldung funktioniert und wo Mehrfachzuordnungen sauber abgefangen werden.

@@ -427,7 +420,7 @@ $canManageTenant = app_can_manage_tenant($auth);
Tenant-Dashboard
-

auf einen Blick

+

Übersicht

Kontostand, Buchungen, Einzahlungen und die letzten Aktivitäten stehen direkt für diesen Tenant bereit.

@@ -528,7 +521,7 @@ $canManageTenant = app_can_manage_tenant($auth);
Mitgliederverwaltung
-

Personen, Admins und Zugänge im Mandanten verwalten

+

Mitglieder

Hier legst du neue Personen an, gibst Zugänge frei und weist bei Bedarf die Rolle als Mandanten-Admin zu.

@@ -605,7 +598,7 @@ $canManageTenant = app_can_manage_tenant($auth);
ZeitMitgliedTypReferenzBetrag
-
Einzahlungen

Zahlungen tenantweit verwalten

Einzahlungen werden direkt in Zahlungstabelle und Ledger geschrieben.

+
Einzahlungen

Zahlungen

Einzahlungen werden direkt in Zahlungstabelle und Ledger geschrieben.

Einzahlung buchen

@@ -622,13 +615,13 @@ $canManageTenant = app_can_manage_tenant($auth);
ZeitMitgliedMethodeBetrag
-
Hinweise und FAQ

Tenant-Inhalte zentral aus der Datenbank

Hinweise und häufige Fragen werden pro Tenant gepflegt und direkt an die Mitglieder ausgespielt.

+
Hinweise und FAQ

Hinweise und FAQ

Hinweise und häufige Fragen werden pro Tenant gepflegt und direkt an die Mitglieder ausgespielt.

Hinweise

Aktuell sind keine Hinweise vorhanden.

Sichtbar bis

FAQ

Aktuell keine FAQ vorhanden.

-
Mandanten-Einstellungen

Einstellungen für Betrieb, Drucklisten und Kommunikation

Diese Einstellungen gelten nur für den aktuell geöffneten Mandanten und bleiben unabhängig von anderen Bereichen.

+
Mandanten-Einstellungen

Einstellungen

Diese Einstellungen gelten nur für den aktuell geöffneten Mandanten und bleiben unabhängig von anderen Bereichen.

Einstellungen speichern

@@ -676,7 +669,7 @@ $canManageTenant = app_can_manage_tenant($auth);
-
Exporte und Drucklisten

Drucklisten als PDF vorbereiten und später aus Papier nacherfassen

Hier erzeugst du druckfertige Listen, speicherst sie als PDF und buchst anschließend die Striche aus Vorder- und Rückseite zurück ins System.

+
Exporte und Drucklisten

Drucklisten

Hier erzeugst du druckfertige Listen, speicherst sie als PDF und buchst anschließend die Striche aus Vorder- und Rückseite zurück ins System.

diff --git a/saas-app/public/support/index.php b/saas-app/public/support/index.php index 7df758a..069b883 100644 --- a/saas-app/public/support/index.php +++ b/saas-app/public/support/index.php @@ -7,6 +7,7 @@ if (session_status() !== PHP_SESSION_ACTIVE) { } require_once __DIR__ . '/../app-support.php'; +require_once __DIR__ . '/../tenant-shell.php'; require_once __DIR__ . '/../../app/Modules/Support/bootstrap.php'; if (!isset($_SESSION['support_csrf'])) { @@ -39,6 +40,7 @@ $flash = app_flash(); $pdo = null; $dbError = null; $supportTablesReady = false; +$tenantLicense = ['plan_name' => 'Free', 'features' => app_feature_defaults()]; $controller = new \App\Modules\Support\Controllers\SupportController(new \App\Modules\Support\Application\SupportService()); $pageData = [ 'title' => 'Support', @@ -63,6 +65,7 @@ $pageData = [ try { $pdo = app_pdo(); + $tenantLicense = app_tenant_license($pdo, (string) $auth['tenant_id']); $supportTablesReady = scripts_table_exists($pdo, 'support_requests') && scripts_table_exists($pdo, 'support_request_messages'); } catch (\Throwable $exception) { @@ -94,6 +97,8 @@ $statuses = $pageData['statuses'] ?? []; $priorities = $pageData['priorities'] ?? []; $routeTargets = $pageData['route_targets'] ?? []; $selectedRequestId = (string) ($_GET['request'] ?? ''); +$tenantNavItems = app_tenant_navigation_items($auth, $tenantLicense); +$tenantNavItems = app_tenant_navigation_items($auth); $template = dirname(__DIR__, 2) . '/resources/views/support/index.blade.php'; if (!is_file($template)) { diff --git a/saas-app/public/surveys/index.php b/saas-app/public/surveys/index.php index 1c94914..9ab5ec5 100644 --- a/saas-app/public/surveys/index.php +++ b/saas-app/public/surveys/index.php @@ -10,6 +10,7 @@ if (session_status() !== PHP_SESSION_ACTIVE) { } require_once dirname(__DIR__) . '/app-support.php'; +require_once dirname(__DIR__) . '/tenant-shell.php'; require_once dirname(__DIR__, 2) . '/app/Modules/Surveys/Domain/Survey.php'; require_once dirname(__DIR__, 2) . '/app/Modules/Surveys/Domain/SurveyQuestion.php'; require_once dirname(__DIR__, 2) . '/app/Modules/Surveys/Domain/SurveyPublication.php'; @@ -32,10 +33,18 @@ if (!function_exists('dt_short')) { } } -$auth = app_auth_user(); +$auth = app_require_auth(); $tenantId = (string) ($auth['tenant_id'] ?? 'tenant-demo'); $tenantName = (string) ($auth['tenant_name'] ?? 'Demo Tenant'); $tenantUser = (string) ($auth['display_name'] ?? 'Survey-Verantwortliche'); +$tenantLicense = ['plan_name' => 'Free', 'features' => app_feature_defaults()]; + +try { + $pdo = app_pdo(); + $tenantLicense = app_tenant_license($pdo, $tenantId); +} catch (\Throwable $ignored) { +} +$tenantNavItems = app_tenant_navigation_items($auth, $tenantLicense); $controller = new SurveyController(new SurveyService()); $payload = $controller->index($tenantId); @@ -48,51 +57,51 @@ $memberBoard = $data['memberBoard']; - Kaffeeliste SaaS - Surveys + Kaffeeliste - Umfragen +HTML; +} + +function tenant_shell_path_is_active(string $currentPath, string $href): bool +{ + $normalize = static function (string $path): string { + $value = parse_url($path, PHP_URL_PATH); + $value = is_string($value) ? rtrim($value, '/') : ''; + + return $value === '' ? '/' : $value; + }; + + return $normalize($currentPath) === $normalize($href); +} + +function tenant_shell_render_header(array $auth, string $currentPath, array $tenantLicense = [], string $title = 'Kaffeeliste', string $subtitle = 'Mandantenbereich'): string +{ + $navItems = app_tenant_navigation_items($auth, $tenantLicense); + $tenantName = (string) ($auth['tenant_name'] ?? 'Tenant'); + $displayName = (string) ($auth['display_name'] ?? 'Benutzer'); + $roleLabel = app_primary_role_label($auth); + $planName = (string) ($tenantLicense['plan_name'] ?? 'Free'); + + ob_start(); + ?> +
+
+

+

+
+
+ + + +
+
+ +
+ + + + + + Global-Admin im Tenant + +
+ 'Free', 'features' => app_feature_defaults()]; if (!app_can_manage_tenant($auth)) { app_flash('Dieser Bereich ist nur für Tenant-Admins oder Global-Admins verfügbar.', 'warning'); @@ -46,6 +48,12 @@ $roleService = new \App\Modules\Tenants\Services\TenantRoleService( ); $controller = new \App\Modules\Tenants\Controllers\TenantConsoleController($tenantService, $roleService); +try { + $pdo = app_pdo(); + $tenantLicense = app_tenant_license($pdo, (string) ($auth['tenant_id'] ?? '')); +} catch (\Throwable $ignored) { +} + $tenantId = (string) ($_GET['tenant'] ?? $auth['tenant_id'] ?? ''); $payload = $controller->roles($tenantId !== '' ? $tenantId : null); $pageData = $payload['data'] ?? []; @@ -57,6 +65,8 @@ $delegationRules = $overview['delegation_rules'] ?? []; $tenantSnapshots = $overview['tenant_snapshots'] ?? []; $permissionGroups = $overview['permission_groups'] ?? []; $notes = $overview['notes'] ?? []; +$tenantNavItems = app_tenant_navigation_items($auth, $tenantLicense); +$tenantNavItems = app_tenant_navigation_items($auth); $template = dirname(__DIR__, 3) . '/resources/views/tenants/roles.blade.php'; if (!is_file($template)) { diff --git a/saas-app/resources/views/content/editor.blade.php b/saas-app/resources/views/content/editor.blade.php index 05e4086..a7ec96d 100644 --- a/saas-app/resources/views/content/editor.blade.php +++ b/saas-app/resources/views/content/editor.blade.php @@ -1,6 +1,6 @@ @extends('layouts.app') -@section('page_title', 'Kaffeeliste SaaS - Content Redaktion') +@section('page_title', 'Kaffeeliste SaaS - Redaktion') @php $editorial = $editorial ?? [ @@ -17,8 +17,8 @@
-

Tenant-Admin-Perspektive

-

Content-Redaktion mit Standardvorlage und Tenant-Override.

+

Redaktion

+

Redaktion

Diese Sicht ist fuer Tenant-Admins gedacht: Hinweise, FAQ und spaetere Textbausteine folgen einer klaren Redaktionslogik mit Freigabe, Sichtbarkeit und tenantbezogener Pflege. diff --git a/saas-app/resources/views/content/index.blade.php b/saas-app/resources/views/content/index.blade.php index 88e0842..208fcb6 100644 --- a/saas-app/resources/views/content/index.blade.php +++ b/saas-app/resources/views/content/index.blade.php @@ -1,6 +1,6 @@ @extends('layouts.app') -@section('page_title', 'Kaffeeliste SaaS - Content') +@section('page_title', 'Kaffeeliste SaaS - Hinweise') @php $data = $content ?? [ @@ -25,8 +25,8 @@

-

Content-MVP

-

Hinweise und FAQ werden zu tenantfaehigen Redaktionsobjekten.

+

Hinweise

+

Hinweise und FAQ

Das Content-Modul zeigt nicht mehr nur Demo-Kacheln, sondern die fachliche Struktur fuer freigabefaehige Hinweise, tenantbezogene FAQ und eine klare diff --git a/saas-app/resources/views/dashboard/index.blade.php b/saas-app/resources/views/dashboard/index.blade.php index 10500e3..f726140 100644 --- a/saas-app/resources/views/dashboard/index.blade.php +++ b/saas-app/resources/views/dashboard/index.blade.php @@ -1,12 +1,12 @@ @extends('layouts.app') -@section('page_title', 'Kaffeeliste SaaS - Dashboard') +@section('page_title', 'Kaffeeliste SaaS - Uebersicht') @section('content')

-

Tenant overview

-

Dein Kaffeeliste-Stand auf einen Blick.

+

Uebersicht

+

Dein Bereich

Das Dashboard zeigt den aktuellen Kontostand, die Nutzung im Monat und die letzten Buchungen. Die Werte sind hier bewusst als fachliche Anker platziert und koennen spaeter direkt aus dem Ledger gespeist werden. diff --git a/saas-app/resources/views/exports/index.blade.php b/saas-app/resources/views/exports/index.blade.php index 1895848..6545034 100644 --- a/saas-app/resources/views/exports/index.blade.php +++ b/saas-app/resources/views/exports/index.blade.php @@ -1,12 +1,12 @@ @extends('layouts.app') -@section('page_title', 'Kaffeeliste SaaS - Exports') +@section('page_title', 'Kaffeeliste SaaS - Exporte') @section('content')

-

Exports

-

Reports und Drucklisten bleiben moeglich, aber nicht mehr als Spezialskript.

+

Exporte

+

Exporte und Drucklisten

Das fruehere PDF fuer die Papierliste wandert in ein Export-Modul. Neben Drucklisten entstehen hier Reports fuer Finance, Mitglieder und diff --git a/saas-app/resources/views/imports/index.blade.php b/saas-app/resources/views/imports/index.blade.php index a270cbe..9e37d5a 100644 --- a/saas-app/resources/views/imports/index.blade.php +++ b/saas-app/resources/views/imports/index.blade.php @@ -1,12 +1,12 @@ @extends('layouts.app') -@section('page_title', 'Kaffeeliste SaaS - Imports') +@section('page_title', 'Kaffeeliste SaaS - Importe') @section('content')

-

Imports

-

Dateiimporte werden kontrollierte Jobs statt einmaliger Root-Skripte.

+

Importe

+

Importe

CSV-Uploads und Legacy-Datenuebernahmen bleiben moeglich, laufen aber als nachvollziehbare Importjobs mit Vorschau, Mapping und Statusmeldungen. diff --git a/saas-app/resources/views/layouts/app.blade.php b/saas-app/resources/views/layouts/app.blade.php index 6a283e7..195e8be 100644 --- a/saas-app/resources/views/layouts/app.blade.php +++ b/saas-app/resources/views/layouts/app.blade.php @@ -5,20 +5,29 @@ @yield('page_title', $title ?? 'Kaffeeliste SaaS') + @php + $layoutAuth = $auth ?? (function_exists('app_auth_user') ? app_auth_user() : null); + $layoutLicense = $tenantLicense ?? ['features' => []]; + $layoutPath = parse_url((string) ($_SERVER['REQUEST_URI'] ?? '/'), PHP_URL_PATH); + $layoutPath = is_string($layoutPath) ? rtrim($layoutPath, '/') : '/'; + $layoutNavItems = function_exists('app_tenant_navigation_items') + ? app_tenant_navigation_items($layoutAuth, $layoutLicense) + : []; + @endphp