Files

252 lines
8.7 KiB
PHP

<?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 = [];
// Export und Rückgabe der eigenen Daten sind Bestandteil von Vertrag und
// AVV und dürfen nicht versehentlich im Back-Office abgeschaltet werden.
if ($featureKey === 'data_export') {
return true;
}
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 = $key === 'data_export' || !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>';
}
/**
* 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 '<div class="hint-box error"><p><b>' . htmlspecialchars($label, ENT_QUOTES, 'UTF-8')
. ' ist in euren Mandant-Einstellungen deaktiviert.</b><br>'
. 'Ein Besitzer oder Administrator kann die Funktion unter '
. '<a href="mandant-einstellungen.php">Mandant-Einstellungen</a> wieder einschalten.</p></div>';
}