Betreiber-Zentralstelle, Mandanten-Design und schlankerer Einstieg
Funktions-Schalter je Mandant (app/features.php, tenant_features): der Betreiber schaltet FAQ, Selbsteintrag, PDF, PayPal-Eingang, CSV-Import, Mailversand, Jahresabschluss, Datenexport und eigenes Design pro Mandant frei. Gesperrte Funktionen verschwinden aus Menue und Schaltflaechen, ihre Seiten weisen Aufrufe und POSTs ab. Das Back-Office ist jetzt fuer Platform-Admins im Menue verlinkt statt nur per URL erreichbar. Eigenes Design je Mandant: Akzentfarbe und Logo in den Mandant- Einstellungen, eingebettet ueber app/branding.php; das Logo liegt geschuetzt in var/tenant_logos und wird nur ueber tenant-logo-anzeigen.php an den eigenen Mandanten ausgeliefert. Weniger Startinformationen: Startpaket neuer Mandanten auf zwei Beispielfragen gekuerzt, Anleitung von ~1400 auf ~750 Woerter gestrafft und um Abschnitte zu gesperrten Funktionen bereinigt. Vorder-/Rueckseite erst bei mehr als 50 Personen (vorher schon ab 50). Die Schwelle liegt jetzt gemeinsam in app/ledger.php und gilt auch fuer die Vorder-/Rueckseiten-Auswahl beim Erfassen, wo sie bisher unabhaengig von der Teamgroesse angeboten wurde. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/bootstrap.php';
|
||||
|
||||
/**
|
||||
* Funktions-Schalter je Mandant, gepflegt vom Betreiber im Back-Office
|
||||
* (backoffice-mandant.php). Sie liegen eine Ebene ueber den
|
||||
* Mandant-Einstellungen: der Kunde stellt dort ein, *wie* eine Funktion
|
||||
* arbeitet - hier entscheidet der Betreiber, ob sie ihm ueberhaupt zur
|
||||
* Verfuegung steht.
|
||||
*
|
||||
* Bewusst kein Bestandteil von saas_user_has_role(): Rollen regeln, wer
|
||||
* innerhalb eines Mandanten etwas darf, Features regeln den Funktionsumfang
|
||||
* des Mandanten als Ganzes. Beide Pruefungen stehen deshalb nebeneinander.
|
||||
*
|
||||
* @return array<string, array{label: string, description: string}>
|
||||
*/
|
||||
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<string, bool>
|
||||
*/
|
||||
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<string, mixed> $submittedFeatures
|
||||
* @return list<string> 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 '<div class="hint-box error"><p><b>' . htmlspecialchars($label, ENT_QUOTES, 'UTF-8')
|
||||
. ' ist für euren Zugang nicht freigeschaltet.</b><br>'
|
||||
. 'Wende dich an den Betreiber der Kaffeeliste, wenn ihr diese Funktion nutzen möchtet.</p></div>';
|
||||
}
|
||||
Reference in New Issue
Block a user