*/ function app_feature_catalog(): array { return [ 'faq' => [ 'label' => 'FAQ-Seite', 'description' => 'Eigene Fragen und Antworten für die Mitglieder des Mandanten.', ], 'self_entry' => [ 'label' => 'Striche selbst eintragen', 'description' => 'Mitglieder tragen ihre Striche im eigenen Dashboard ein (zusätzlich zur Mandant-Einstellung „Web-Striche erlauben").', ], 'pdf_export' => [ 'label' => 'Kaffeeliste als PDF', 'description' => 'Ausdruck der Strichliste zum Aufhängen.', ], 'paypal_inbox' => [ 'label' => 'PayPal-Zahlungseingang', 'description' => 'Automatisches Verbuchen weitergeleiteter PayPal-Mails samt Zuordnungs-Warteschlange.', ], 'csv_import' => [ 'label' => 'CSV-Import', 'description' => 'Einlesen von Einzahlungen und Mitgliedern aus einer Datei.', ], 'mail_dispatch' => [ 'label' => 'Info-Mails und Erinnerungen', 'description' => 'Sammelmails an Mitglieder und automatische Zahlungserinnerungen.', ], 'year_closing' => [ 'label' => 'Jahresabschluss', 'description' => 'Verteilen eines Gesamtbetrags als Guthaben auf alle Mitglieder.', ], 'data_export' => [ 'label' => 'Datenexport', 'description' => 'Herunterladen aller Mandantendaten durch den Kunden selbst.', ], 'branding' => [ 'label' => 'Eigenes Design', 'description' => 'Eigenes Logo und eigene Akzentfarbe in der App-Oberfläche des Mandanten.', ], ]; } /** * Standard fuer jede Funktion, wenn der Betreiber nichts abweichend gesetzt * hat. Alles ist freigeschaltet - ein neuer Kunde soll den vollen * Funktionsumfang bekommen, ohne dass der Betreiber erst etwas einschalten * muss. */ function app_feature_default(string $featureKey): bool { return array_key_exists($featureKey, app_feature_catalog()); } /** * Aktueller Stand aller bekannten Funktionen fuer einen Mandanten. * * @return array */ function app_features_for_tenant(PDO $pdo, int $tenantId): array { $state = []; foreach (array_keys(app_feature_catalog()) as $key) { $state[$key] = app_feature_default($key); } try { $stmt = $pdo->prepare('SELECT feature_key, enabled FROM tenant_features WHERE tenant_id = ?'); $stmt->execute([$tenantId]); foreach ($stmt->fetchAll() as $row) { $key = (string)$row['feature_key']; if (array_key_exists($key, $state)) { $state[$key] = (int)$row['enabled'] === 1; } } } catch (Throwable $e) { // Fehlt die Tabelle (Migration noch nicht eingespielt), bleibt es bei // den Standardwerten - eine nicht ausgerollte Migration darf keine // Funktion sperren. return $state; } return $state; } /** * Einzelne Funktion pruefen. Ergebnis wird pro Request zwischengespeichert, * damit Seiten mit mehreren Pruefungen (Navigation!) nicht je Aufruf erneut * die Datenbank befragen. */ function app_feature_enabled(PDO $pdo, int $tenantId, string $featureKey): bool { static $cache = []; if ($tenantId <= 0) { return app_feature_default($featureKey); } if (!array_key_exists($tenantId, $cache)) { $cache[$tenantId] = app_features_for_tenant($pdo, $tenantId); } return $cache[$tenantId][$featureKey] ?? app_feature_default($featureKey); } /** * Setzt die Schalter eines Mandanten neu. Erwartet die Formulardaten des * Back-Office-Formulars: jede angehakte Funktion steht als Schluessel in * $input['features']. * * @param array $submittedFeatures * @return list Liste der geaenderten Funktionsschluessel */ function app_features_update(PDO $pdo, int $tenantId, array $submittedFeatures): array { $before = app_features_for_tenant($pdo, $tenantId); $changed = []; $stmt = $pdo->prepare( 'INSERT INTO tenant_features (tenant_id, feature_key, enabled) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE enabled = VALUES(enabled)' ); foreach (array_keys(app_feature_catalog()) as $key) { $enabled = !empty($submittedFeatures[$key]); $stmt->execute([$tenantId, $key, $enabled ? 1 : 0]); if (($before[$key] ?? true) !== $enabled) { $changed[] = $key; } } return $changed; } /** * Seitenwaechter: bricht mit einer verstaendlichen Meldung ab, wenn der * Betreiber die Funktion fuer diesen Mandanten abgeschaltet hat. Die Meldung * wird bewusst in die bereits geoeffnete Seite geschrieben, damit sie im * gewohnten Layout erscheint statt als nackter Text. */ function app_feature_notice_html(string $featureKey): string { $label = app_feature_catalog()[$featureKey]['label'] ?? $featureKey; return '

' . htmlspecialchars($label, ENT_QUOTES, 'UTF-8') . ' 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.

'; }