From 3f6866e0750ee06e0b5320d9dda4467ed5b0bbe1 Mon Sep 17 00:00:00 2001
From: Clemens Creutzburg
Date: Thu, 20 Aug 2026 23:16:30 +0200
Subject: [PATCH] Menue folgt auch den Schaltern des Mandanten
Bisher blendete das Menue nur aus, was der Betreiber gesperrt hatte. Hat
der Mandant selbst eine Funktion abgeschaltet - etwa "PayPal anbieten" in
den Mandant-Einstellungen - blieb der Menuepunkt stehen und fuehrte auf
eine Seite ohne Zweck.
app_feature_available() prueft nun beide Ebenen, app_feature_tenant_switch()
haelt die Zuordnung Funktion -> Mandanten-Schalter. Navigation und Anleitung
nutzen die neue Pruefung; paypal-zuordnung.php sperrt sich beim direkten
Aufruf ebenfalls und verweist dabei auf die Mandant-Einstellungen statt auf
den Betreiber.
Co-Authored-By: Claude Opus 5
---
anleitung.php | 6 ++--
app/features.php | 78 ++++++++++++++++++++++++++++++++++++++++++++
docs/backoffice.md | 11 +++++++
footer.php | 8 +++--
paypal-zuordnung.php | 19 +++++++----
5 files changed, 110 insertions(+), 12 deletions(-)
diff --git a/anleitung.php b/anleitung.php
index 90ac5a9..83ea6ed 100644
--- a/anleitung.php
+++ b/anleitung.php
@@ -17,11 +17,11 @@ $istAdmin = $saasUser !== null && saas_user_has_role(['owner', 'admin'], $saasUs
// Einstellungen fuer die Hinweise zu den aktivierten Zahlungswegen.
$settings = $saasUser !== null ? saas_fetch_tenant_settings($pdo, (int)$saasUser['tenant_id']) : null;
-// Gleiche Logik wie im Menue: was der Betreiber fuer diesen Mandanten
-// gesperrt hat, wird hier auch nicht erklaert.
+// Gleiche Logik wie im Menue: was der Betreiber gesperrt oder der Mandant
+// selbst abgeschaltet hat, wird hier auch nicht erklaert.
$anleitungTenantId = $saasUser !== null ? (int)$saasUser['tenant_id'] : 0;
$kann = static function (string $feature) use ($pdo, $anleitungTenantId): bool {
- return $anleitungTenantId <= 0 || app_feature_enabled($pdo, $anleitungTenantId, $feature);
+ return $anleitungTenantId <= 0 || app_feature_available($pdo, $anleitungTenantId, $feature);
};
include "header.php";
diff --git a/app/features.php b/app/features.php
index 5f75c8e..4c72ad6 100644
--- a/app/features.php
+++ b/app/features.php
@@ -165,3 +165,81 @@ function app_feature_notice_html(string $featureKey): string
. ' ist für euren Zugang nicht freigeschaltet.
'
. 'Wende dich an den Betreiber der Kaffeeliste, wenn ihr diese Funktion nutzen möchtet.
';
}
+
+/**
+ * Zuordnung Betreiber-Funktion -> Schalter in den Mandant-Einstellungen.
+ * Manche Funktionen kann der Kunde zusaetzlich selbst abschalten; wer PayPal
+ * gar nicht als Zahlungsweg anbietet, braucht auch keine PayPal-Zuordnung.
+ * Funktionen ohne Eintrag kennen nur den Betreiber-Schalter.
+ */
+function app_feature_tenant_switch(string $featureKey): ?string
+{
+ $switches = [
+ 'paypal_inbox' => 'paypal_enabled',
+ 'self_entry' => 'self_entry_enabled',
+ ];
+
+ return $switches[$featureKey] ?? null;
+}
+
+/**
+ * Ist eine Funktion fuer den Mandanten tatsaechlich nutzbar? Prueft beide
+ * Ebenen: erst die Freischaltung durch den Betreiber, dann - falls vorhanden -
+ * den eigenen Schalter des Kunden in den Mandant-Einstellungen.
+ */
+function app_feature_available(PDO $pdo, int $tenantId, string $featureKey): bool
+{
+ if (!app_feature_enabled($pdo, $tenantId, $featureKey)) {
+ return false;
+ }
+
+ $switch = app_feature_tenant_switch($featureKey);
+ if ($switch === null || $tenantId <= 0) {
+ return true;
+ }
+
+ return app_tenant_switch_enabled($pdo, $tenantId, $switch);
+}
+
+/**
+ * Einzelnen Schalter aus den Mandant-Einstellungen lesen. Wie bei den
+ * Betreiber-Funktionen wird das Ergebnis pro Request gehalten, damit die
+ * Navigation die Einstellungen nicht mehrfach laedt.
+ */
+function app_tenant_switch_enabled(PDO $pdo, int $tenantId, string $settingKey): bool
+{
+ static $cache = [];
+
+ if (!array_key_exists($tenantId, $cache)) {
+ try {
+ require_once __DIR__ . '/saas-auth.php';
+ $cache[$tenantId] = saas_fetch_tenant_settings($pdo, $tenantId) ?? [];
+ } catch (Throwable $e) {
+ // Lassen sich die Einstellungen nicht lesen, entscheidet allein die
+ // Betreiber-Freischaltung - kein stiller Funktionsverlust.
+ $cache[$tenantId] = [];
+ }
+ }
+
+ if (!array_key_exists($settingKey, $cache[$tenantId])) {
+ return true;
+ }
+
+ return (int)$cache[$tenantId][$settingKey] === 1;
+}
+
+/**
+ * Gegenstueck zu app_feature_notice_html() fuer den Fall, dass nicht der
+ * Betreiber, sondern der Mandant selbst die Funktion abgeschaltet hat. Hier
+ * hilft kein Kontakt zum Betreiber, sondern der Weg in die eigenen
+ * Einstellungen.
+ */
+function app_feature_tenant_notice_html(string $featureKey): string
+{
+ $label = app_feature_catalog()[$featureKey]['label'] ?? $featureKey;
+
+ return '' . htmlspecialchars($label, ENT_QUOTES, 'UTF-8')
+ . ' ist in euren Mandant-Einstellungen deaktiviert.
'
+ . 'Ein Besitzer oder Administrator kann die Funktion unter '
+ . 'Mandant-Einstellungen wieder einschalten.
';
+}
diff --git a/docs/backoffice.md b/docs/backoffice.md
index 975cee8..050f01e 100644
--- a/docs/backoffice.md
+++ b/docs/backoffice.md
@@ -94,6 +94,17 @@ Design-Entscheidungen:
einem Hinweis statt mit einem Fehler. Die POST-Verarbeitung der
betroffenen Seiten hängt an derselben Prüfung, eine gesperrte Funktion
lässt sich also auch nicht per Formular-POST auslösen.
+- **Abgeschaltet vom Kunden heißt ebenfalls unsichtbar.** Für Funktionen
+ mit eigenem Schalter in den Mandant-Einstellungen zieht `footer.php`
+ (und die Anleitung) `app_feature_available()` heran: erst die
+ Freischaltung durch den Betreiber, dann der Schalter des Kunden. Die
+ Zuordnung steht in `app_feature_tenant_switch()` – aktuell
+ `paypal_inbox` → `paypal_enabled` und `self_entry` →
+ `self_entry_enabled`. Wer PayPal nicht als Zahlungsweg anbietet, sieht
+ den Menüpunkt „PayPal-Zahlungen" also gar nicht erst; beim direkten
+ Aufruf erscheint statt des Betreiber-Hinweises
+ `app_feature_tenant_notice_html()` mit dem Weg zurück in die
+ Mandant-Einstellungen.
- Jede Änderung landet als `backoffice.features_updated` im Audit-Log des
betroffenen Mandanten – wie jeder andere Back-Office-Zugriff auch.
diff --git a/footer.php b/footer.php
index 305f208..72f6f89 100644
--- a/footer.php
+++ b/footer.php
@@ -31,8 +31,10 @@ if ($saasNavUser !== null && function_exists('saas_list_user_memberships')) {
}
}
-// Vom Betreiber gesperrte Funktionen erscheinen gar nicht erst im Menue -
-// ein Menuepunkt, der nur zu einer Absage fuehrt, ist schlechter als keiner.
+// Gesperrte Funktionen erscheinen gar nicht erst im Menue - ein Menuepunkt,
+// der nur zu einer Absage fuehrt, ist schlechter als keiner. Geprueft werden
+// beide Ebenen: die Freischaltung durch den Betreiber und der eigene Schalter
+// des Mandanten in den Mandant-Einstellungen (z. B. "PayPal anbieten").
require_once __DIR__ . '/app/features.php';
$saasNavTenantId = $saasNavUser !== null ? (int)$saasNavUser['tenant_id'] : 0;
$saasNavFeature = static function (string $key) use ($saasNavTenantId): bool {
@@ -40,7 +42,7 @@ $saasNavFeature = static function (string $key) use ($saasNavTenantId): bool {
return true;
}
- return app_feature_enabled(app_db_pdo(), $saasNavTenantId, $key);
+ return app_feature_available(app_db_pdo(), $saasNavTenantId, $key);
};
// Das Back-Office war bisher nur ueber die direkte URL erreichbar. Es ist die
diff --git a/paypal-zuordnung.php b/paypal-zuordnung.php
index a0d1314..8cac45e 100644
--- a/paypal-zuordnung.php
+++ b/paypal-zuordnung.php
@@ -16,11 +16,18 @@ if ($saasUser !== null && saas_user_has_role(['owner', 'admin', 'treasurer'], $s
$hasAccess = true;
}
-// Vom Betreiber gesperrte Funktion: $hasAccess faellt zurueck, damit auch die
-// POST-Verarbeitung weiter unten nicht mehr laeuft.
-$gesperrteFunktion = null;
+// Gesperrte Funktion: $hasAccess faellt zurueck, damit auch die
+// POST-Verarbeitung weiter unten nicht mehr laeuft. Der Menuepunkt ist in
+// diesem Fall ausgeblendet - die Seite bleibt aber ueber die direkte URL
+// erreichbar und muss deshalb selbst pruefen.
+$sperrHinweis = null;
if ($hasAccess && !app_feature_enabled($pdo, $tenantId, 'paypal_inbox')) {
- $gesperrteFunktion = 'paypal_inbox';
+ // Der Betreiber hat die Funktion fuer diesen Mandanten nicht freigeschaltet.
+ $sperrHinweis = app_feature_notice_html('paypal_inbox');
+ $hasAccess = false;
+} elseif ($hasAccess && !app_feature_available($pdo, $tenantId, 'paypal_inbox')) {
+ // Der Mandant selbst bietet PayPal nicht als Zahlungsweg an.
+ $sperrHinweis = app_feature_tenant_notice_html('paypal_inbox');
$hasAccess = false;
}
@@ -130,8 +137,8 @@ include "nav.php";