diff --git a/anleitung.php b/anleitung.php index e263dd4..90ac5a9 100644 --- a/anleitung.php +++ b/anleitung.php @@ -2,6 +2,7 @@ include "functions.php"; require_once __DIR__ . "/app/ledger.php"; +require_once __DIR__ . "/app/features.php"; $pdo = app_db_pdo(); $saasUser = saas_current_user($pdo); @@ -16,6 +17,13 @@ $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. +$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); +}; + include "header.php"; include "headerline.php"; include "nav.php"; @@ -25,8 +33,8 @@ include "nav.php";

Anleitung

-

Diese Seite erklärt die wichtigsten Funktionen der Kaffeeliste. Oben stehen - die Punkte, die jedes Mitglied betreffen, darunter die Verwaltungsfunktionen.

+

Kurzüberblick über die Funktionen. Oben steht, was jedes Mitglied betrifft, + darunter die Verwaltung.

@@ -40,34 +48,20 @@ include "nav.php";

Für alle Mitglieder

Meine Kaffeeliste

-

Das ist deine Startseite. Sie zeigt deinen aktuellen Stand, deine - Jahresübersicht sowie die letzten Einzahlungen und Striche.

- -

In der Monatsübersicht siehst du, wie viele Striche in welchem Monat - zusammengekommen sind.

+

Deine Startseite zeigt den aktuellen Stand, die Jahres- und Monatsübersicht sowie + die letzten Buchungen. Positiver Betrag = Guthaben, negativer Betrag = offener Betrag.

+

Striche selbst eintragen

-

Wenn die Verwaltung das freigeschaltet hat, kannst du deinen Kaffee direkt auf - der Startseite eintragen – ein oder zwei Striche pro Vorgang.

-
-

Vertippt? Direkt darunter kannst du deinen zuletzt selbst - eingetragenen Strich wieder stornieren. Striche, die der Kassenwart über die - Sammelerfassung eingetragen hat, kannst du nicht selbst stornieren – wende dich - dafür an die Verwaltung.

-
+

Wenn die Verwaltung das freigeschaltet hat, trägst du deinen Kaffee direkt auf der + Startseite ein. Vertippt? Mit „Letzten Strich stornieren" nimmst du deinen jüngsten + selbst eingetragenen Strich zurück – vom Kassenwart erfasste Striche nicht.

+

Bezahlen

-

Welche Wege zur Verfügung stehen, legt die Verwaltung fest. Möglich sind:

- +

Welche Wege offen stehen, legt die Verwaltung fest: Barzahlung, PayPal oder Überweisung.

-

In eurem Mandanten sind aktuell aktiv: +

Bei euch aktiv: .

-

Wichtig bei PayPal: Nutzt die Funktion „Geld an Freunde senden“. Bei - Zahlungen für „Waren und Dienstleistungen“ zieht PayPal Gebühren ab – gutgeschrieben - wird dann nur der Betrag, der tatsächlich angekommen ist.

+

Wichtig bei PayPal: „Geld an Freunde senden" nutzen. Bei „Waren und + Dienstleistungen" zieht PayPal Gebühren ab – gutgeschrieben wird nur, was ankommt.

-

Anzeigenamen ändern

-

Unter Namensanpassung kannst du festlegen, wie dein Name auf der - Kaffeeliste und im Ausdruck erscheint (maximal 20 Zeichen).

- -

Kundenkonto

-

Unter Kundenkonto findest du deine Zugangsdaten, kannst deine E-Mail-Adresse - bestätigen und dein Passwort ändern.

+

Eigene Angaben

+

Unter Namensanpassung legst du fest, wie dein Name in der Liste und im Ausdruck + erscheint (max. 20 Zeichen). Unter Kundenkonto änderst du Passwort und E-Mail.


Für Kassenwart und Verwaltung

-

Sichtbar für die Rollen Kassenwart, Administrator und - Inhaber.

-

Einzahlung eintragen

-

Trage hier ein, wer wie viel eingezahlt hat. Du kannst mehrere Mitglieder in - einem Durchgang erfassen; leere Felder werden übersprungen.

+

Buchen

    -
  • Bemerkung: optionale Notiz, damit später nachvollziehbar bleibt, worum es ging.
  • -
  • Negativer Betrag = Abzug (z. B. Auszahlung bei Austritt oder eine - Erstattung). Dabei ist eine Bemerkung Pflicht.
  • +
  • Einzahlung eintragen: mehrere Mitglieder in einem Durchgang; leere Felder + werden übersprungen. Negativer Betrag = Abzug, dann ist eine Bemerkung Pflicht.
  • +
  • Striche eintragen: Sammelerfassung für die Papierliste. Der Preis pro Strich + kommt aus den Einstellungen und ist pro Vorgang überschreibbar.
  • +
  • Letzte Einträge: falsche Buchungen hier stornieren, nicht per + Gegenbuchung ausgleichen – sonst zählen beide als Umsatz in der Auswertung.
-

Falsch eingetragen ist kein Abzug. Wenn eine Buchung schlicht falsch war, - storniere sie unter Letzte Einträge. Ein Gegen-Abzug würde die Auswertung - verfälschen, weil dann beide Buchungen als Umsatz zählen.

Zum Schutz vor Vertippern werden Beträge über 1.000 € abgelehnt. Schlägt eine Zeile fehl, wird nichts gebucht und deine Eingaben bleiben stehen.

-

Striche eintragen

-

Sammelerfassung für die Papierliste. Über die Schaltflächen wählst du:

-
    -
  • Vorderseite: Vieltrinker – Mitglieder mit mindestens 10 Strichen im Listenfenster.
  • -
  • Rückseite: alle übrigen aktiven Mitglieder.
  • -
  • Alle: die komplette Liste.
  • -
-

Der Preis pro Strich wird aus den Mandant-Einstellungen vorbelegt und kann - für den einzelnen Vorgang überschrieben werden.

- -

Letzte Einträge – stornieren

-

Zeigt die letzten 100 Einzahlungen und Striche. Fehlerhafte Buchungen werden - storniert, nicht gelöscht: Sie verschwinden aus der Auswertung, bleiben aber - im Journal nachvollziehbar.

- +

PayPal-Zahlungen automatisch verbuchen

-

Die Kaffeeliste kann PayPal-Zahlungen selbstständig gutschreiben:

-
    -
  1. Auf der Seite PayPal-Zahlungen steht eure persönliche Eingangsadresse.
  2. -
  3. Richtet in eurem Mailprogramm eine automatische Weiterleitung der - PayPal-Benachrichtigungen an diese Adresse ein.
  4. -
  5. Erkannte Zahlungen werden dem passenden Mitglied automatisch gutgeschrieben.
  6. -
-

Gutgeschrieben wird immer der Betrag, der tatsächlich angekommen ist – bei - Waren & Dienstleistungen also nach Abzug der PayPal-Gebühr.

-
-

Zahlungen, die sich nicht eindeutig zuordnen lassen, landen in der Warteschlange - „Offene Zahlungen“ und können dort von Hand einem Mitglied zugeordnet oder ignoriert - werden. Damit die automatische Zuordnung möglichst oft greift, hinterlege den - PayPal-Namen beim Mitglied.

-
+

Auf der Seite PayPal-Zahlungen steht eure Eingangsadresse. Leitet die + PayPal-Benachrichtigungen aus eurem Postfach automatisch dorthin weiter, dann werden + erkannte Zahlungen selbstständig gutgeschrieben – immer der tatsächlich angekommene + Betrag. Nicht eindeutig zuordenbare Zahlungen landen in der Warteschlange „Offene + Zahlungen". Ein hinterlegter PayPal-Name beim Mitglied verbessert die Trefferquote.

+ -

CSV-Import

-

Über CSV Upload lassen sich Einzahlungen aus einer Datei einlesen. Der Import - zeigt zuerst eine Vorschau; erst danach wird gebucht. Zeilen ohne eindeutige - Zuordnung werden markiert und nicht verbucht.

- -

Kaffeeliste, Ausdruck und Auswertung

+

Auswertung und Ausdruck

    -
  • Kaffeeliste anzeigen: Gesamtübersicht aller aktiven Mitglieder. Ein Klick - auf den Namen öffnet die Einzelauswertung.
  • -
  • PDF-Export: die Liste zum Ausdrucken und Aufhängen.
  • -
  • Journal-Vorschau: alle Buchungen mit Herkunft – nützlich zur Kontrolle.
  • +
  • Kaffeeliste anzeigen: Gesamtübersicht; ein Klick auf den Namen öffnet die + Einzelauswertung.
  • + +
  • PDF-Export: die Liste zum Aufhängen. Bis 50 Mitglieder eine Seite, darüber + Vorder- und Rückseite.
  • + +
  • Journal-Vorschau: alle Buchungen mit Herkunft.
  • + +
  • CSV Upload: Einzahlungen aus einer Datei einlesen – erst Vorschau, dann + buchen. Zeilen ohne eindeutige Zuordnung werden nicht verbucht.
  • +
-

Info-Mails versenden

-

Unter Mailversand lassen sich Mitglieder über ihren Stand informieren. Der - Dry-Run zeigt zuerst, wer eine Mail bekäme, ohne etwas zu verschicken. Das - Versandlog hält fest, was tatsächlich rausging.

- -

Jahresabschluss

-

Verteilt einen frei wählbaren Gesamtbetrag als Guthaben auf alle Mitglieder – - anteilig zu ihren Strichen im laufenden Jahr. Auch hier gilt: erst Dry-Run - zur Vorschau, dann buchen. Beim echten Lauf werden die Mitglieder per Mail informiert.

+ +

Mails und Jahresabschluss

+
    + +
  • Info-Mail versenden: informiert Mitglieder über ihren Stand. Der Dry-Run + zeigt vorher, wer eine Mail bekäme.
  • + + +
  • Jahresabschluss: verteilt einen frei wählbaren Betrag anteilig zu den + Strichen des Jahres als Guthaben. Auch hier erst Dry-Run, dann buchen.
  • + +
+

Für Administratoren

-

Sichtbar für die Rollen Administrator und Inhaber.

Mitglieder verwalten

    -
  • Anlegen und bearbeiten von Mitgliedern (Name, E-Mail, PayPal-Name).
  • -
  • PayPal-Name: entscheidend für die automatische Zuordnung von Zahlungen.
  • -
  • Deaktivieren statt löschen: Das Mitglied verschwindet aus den Listen, - die Buchungshistorie bleibt erhalten.
  • -
  • Anonymisieren: ersetzt Name und E-Mail durch einen Platzhalter (DSGVO-Auskunft), - die Buchungen der Kasse bleiben für die Abrechnung bestehen.
  • -
  • Zugang gewähren / entziehen: steuert, ob ein Mitglied sich anmelden kann.
  • -
  • CSV-Import für viele Mitglieder auf einmal – eine passende Vorlage gibt es - zum Herunterladen.
  • +
  • Deaktivieren statt löschen: das Mitglied verschwindet aus den Listen, die + Buchungshistorie bleibt.
  • +
  • Anonymisieren: ersetzt Name und E-Mail durch einen Platzhalter, die Buchungen + der Kasse bleiben bestehen.
  • +
  • Zugang gewähren / entziehen steuert, ob sich jemand anmelden kann.
-

Hinweise verwalten

-

Ein Hinweis erscheint als gelber Balken oben auf jeder Seite – z. B. „Kaffeekasse - ist leer“. Jeder Hinweis hat ein Gültig bis-Datum und verschwindet danach - automatisch.

+

Hinweise

+

Ein Hinweis erscheint als gelber Balken oben auf jeder Seite und verschwindet nach dem + Gültig bis-Datum automatisch.

Mandant-Einstellungen

-

Hier stellst du das Verhalten der Kaffeeliste ein:

-
    -
  • Preis pro Strich – Grundlage jeder Verbrauchsbuchung.
  • -
  • Listenfenster in Tagen – Zeitraum für die Vorder-/Rückseiten-Aufteilung.
  • -
  • Schulden-Warnschwelle – ab welchem Minus gewarnt bzw. erinnert wird.
  • -
  • Bezahloptionen – Barzahlung, PayPal und Überweisung samt Kontaktdaten. - Nur aktivierte Wege erscheinen bei den Mitgliedern.
  • -
  • Web-Striche erlauben – ob Mitglieder selbst eintragen dürfen.
  • -
  • Ausdruck – Sortierung, Zeilenhöhe, leere Zeilen, Wasserzeichen und Fußzeile.
  • -
  • Zahlungserinnerungen – automatische Mails an Mitglieder unterhalb der - Warnschwelle, samt Mindestabstand zwischen zwei Erinnerungen.
  • -
-

Im Protokoll unten auf der Seite steht, wer wann welche Änderung vorgenommen hat.

+

Preis pro Strich, Listenfenster, Warnschwelle, Bezahloptionen, Web-Striche, Gestaltung + des Ausdrucks + und Zahlungserinnerungen. Unten steht das Protokoll aller Admin-Änderungen.

Konto, Abo und Daten

  • Kundenkonto: Tarif, Rechnungen und Zahlungsdaten.
  • -
  • Datenexport: alle Daten des Mandanten als Datei – für ein Backup oder - einen Umzug.
  • -
  • Mandant löschen: entfernt den Mandanten mit allen Daten endgültig.
  • + +
  • Datenexport: alle Daten des Mandanten als Datei.
  • + +
  • Mandant löschen: entfernt alles endgültig.
-
-

Das Löschen eines Mandanten kann nicht rückgängig gemacht werden. Lade dir - vorher über den Datenexport eine Kopie herunter.

-

Rollen im Überblick

@@ -231,11 +180,10 @@ include "nav.php";
+
- -

Noch Fragen?

-

Häufige Fragen und die Antworten eurer Verwaltung stehen unter - FAQ Kaffeeliste.

+

Weitere Fragen beantwortet eure Verwaltung unter FAQ Kaffeeliste.

+
diff --git a/app/branding.php b/app/branding.php new file mode 100644 index 0000000..443529d --- /dev/null +++ b/app/branding.php @@ -0,0 +1,105 @@ + '', 'logo_url' => '', 'tenant_name' => '']; + + try { + $user = saas_current_user($pdo); + if ($user === null) { + return $cached = $leer; + } + + $tenantId = (int)$user['tenant_id']; + if (!app_feature_enabled($pdo, $tenantId, 'branding')) { + return $cached = $leer; + } + + $settings = saas_fetch_tenant_settings($pdo, $tenantId); + if ($settings === null) { + return $cached = $leer; + } + + $color = (string)($settings['brand_color'] ?? ''); + // Zweite Pruefung neben der Formularvalidierung: was hier nicht wie + // ein Hex-Wert aussieht, wird nie in ein Stylesheet geschrieben. + if (!preg_match('/^#[0-9a-fA-F]{6}$/', $color)) { + $color = ''; + } + + return $cached = [ + 'color' => strtolower($color), + 'logo_url' => trim((string)($settings['brand_logo'] ?? '')) !== '' ? 'tenant-logo-anzeigen.php' : '', + 'tenant_name' => (string)($settings['name'] ?? ''), + ]; + } catch (Throwable $e) { + // Branding ist Beiwerk: faellt es aus (z. B. Migration noch nicht + // eingespielt), laedt die Seite trotzdem im Standarddesign. + return $cached = $leer; + } +} + +/** + * Erzeugt die Farbueberschreibungen fuer das Template-Stylesheet. main.css + * verwendet durchgaengig genau einen Akzentton (#38761d) - hier werden die + * sichtbaren Stellen davon auf die Mandantenfarbe umgestellt. + */ +function app_branding_css(string $color): string +{ + $c = preg_match('/^#[0-9a-f]{6}$/', strtolower($color)) ? strtolower($color) : '#38761d'; + + return << :last-child { border-bottom-color: var(--brand-color); } + input[type="text"]:focus, input[type="password"]:focus, input[type="email"]:focus, + input[type="tel"]:focus, input[type="search"]:focus, input[type="url"]:focus, + input[type="number"]:focus, input[type="date"]:focus, input[type="datetime-local"]:focus, + select:focus, textarea:focus, + input[type="checkbox"]:focus + label:before, input[type="radio"]:focus + label:before { + border-color: var(--brand-color); + box-shadow: 0 0 0 1px var(--brand-color); + } + input[type="checkbox"]:checked + label:before, input[type="radio"]:checked + label:before { + background: var(--brand-color); + border-color: var(--brand-color); + } + input[type="submit"], input[type="reset"], input[type="button"], button, .button { + box-shadow: inset 0 0 0 2px var(--brand-color); + color: var(--brand-color) !important; + } + input[type="submit"].primary, input[type="reset"].primary, input[type="button"].primary, + button.primary, .button.primary { + background-color: var(--brand-color); + box-shadow: none; + color: #ffffff !important; + } + #header { border-bottom-color: var(--brand-color); } + #menu ul a:hover, #menu ul span:hover { color: var(--brand-color); } + .tenant-brand-line { border-bottom: solid 1px rgba(0, 0, 0, 0.08); } + +CSS; +} diff --git a/app/faq.php b/app/faq.php index dc107dd..19522f1 100644 --- a/app/faq.php +++ b/app/faq.php @@ -65,18 +65,17 @@ function faq_soft_delete(PDO $pdo, int $tenantId, int $id): bool } /** - * Seeds a small, generic starter FAQ for a brand-new tenant (mirrors the - * excerpt already shown on the public landing page), so new customers do - * not start with a completely empty FAQ. They can edit or delete every - * entry afterwards. + * Seeds two example questions for a brand-new tenant, damit die FAQ-Seite + * nicht voellig leer startet und sofort erkennbar ist, wie ein Eintrag + * aussieht. Bewusst nur zwei Beispiele statt eines fertigen Katalogs: die + * Antworten sind Platzhalter, die jeder Mandant ohnehin auf seine eigene + * Teekueche umschreibt. Beide Eintraege sind frei editier- und loeschbar. */ function faq_seed_default_entries(PDO $pdo, int $tenantId): void { $starters = [ - ['Wie starten wir mit unserer Kaffeeliste?', 'Mitglieder unter „Mitglieder verwalten" anlegen, danach direkt Striche und Einzahlungen erfassen. Es ist keine weitere Einrichtung nötig.'], - ['Wer kann Mitglieder verwalten oder Einstellungen ändern?', 'Das hängt von der zugewiesenen Rolle ab: Administrator und Kassenwart können Buchungen vornehmen, nur Administrator und Inhaber können Einstellungen und Zugänge verwalten.'], - ['Können wir per PayPal bezahlen?', 'Ja, wenn PayPal in den Mandant-Einstellungen aktiviert ist. Einzahlungen lassen sich zusätzlich jederzeit manuell erfassen.'], - ['Wie erfahre ich meinen aktuellen Stand?', 'Im eigenen Dashboard unter „Meine Kaffeeliste" ist der aktuelle Stand jederzeit sichtbar.'], + ['Wie starten wir mit unserer Kaffeeliste?', 'Beispielantwort – bitte anpassen: Mitglieder unter „Mitglieder verwalten" anlegen, danach direkt Striche und Einzahlungen erfassen.'], + ['Wie erfahre ich meinen aktuellen Stand?', 'Beispielantwort – bitte anpassen: Unter „Meine Kaffeeliste" ist der aktuelle Stand jederzeit sichtbar.'], ]; $stmt = $pdo->prepare('INSERT INTO faq_entries (tenant_id, question, answer, sort_order) VALUES (?, ?, ?, ?)'); diff --git a/app/features.php b/app/features.php new file mode 100644 index 0000000..5f75c8e --- /dev/null +++ b/app/features.php @@ -0,0 +1,167 @@ + + */ +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.

'; +} diff --git a/app/ledger.php b/app/ledger.php index 36a809a..87ee4eb 100644 --- a/app/ledger.php +++ b/app/ledger.php @@ -523,6 +523,21 @@ function ledger_record_payment(PDO $pdo, int $tenantId, int $participantId, int return (int)$pdo->lastInsertId(); } +/** + * Bis zu dieser Mitgliederzahl passt die Strichliste auf ein einzelnes Blatt. + * Erst darueber wird sie auf Vorder- und Rueckseite aufgeteilt - und nur dann + * ergeben auch die Vorder-/Rueckseiten-Ansichten beim Erfassen einen Sinn. + */ +const LEDGER_SINGLE_PAGE_MAX_MEMBERS = 50; + +/** + * Braucht dieser Mandant eine zweiseitige Liste (Vorder- und Rueckseite)? + */ +function ledger_uses_two_sided_sheet(int $activeMemberCount): bool +{ + return $activeMemberCount > LEDGER_SINGLE_PAGE_MAX_MEMBERS; +} + /** * "100-Tage-Liste" Vorder-/Rueckseiten-Aufteilung: aktive Teilnehmer, deren * summierte Striche im nachlaufenden Fenster die Zehner-Schwelle erreichen diff --git a/app/saas-auth.php b/app/saas-auth.php index e3339fb..f6fc952 100644 --- a/app/saas-auth.php +++ b/app/saas-auth.php @@ -286,7 +286,9 @@ function saas_fetch_tenant_settings(PDO $pdo, int $tenantId): ?array ts.cash_contact, ts.bank_transfer_enabled, ts.bank_account_holder, - ts.bank_iban + ts.bank_iban, + ts.brand_logo, + ts.brand_color FROM tenants t LEFT JOIN tenant_settings ts ON ts.tenant_id = t.id WHERE t.id = ? @@ -332,6 +334,8 @@ function saas_fetch_tenant_settings(PDO $pdo, int $tenantId): ?array 'bank_transfer_enabled' => (int)$settings['bank_transfer_enabled'], 'bank_account_holder' => (string)$settings['bank_account_holder'], 'bank_iban' => (string)$settings['bank_iban'], + 'brand_logo' => (string)$settings['brand_logo'], + 'brand_color' => (string)$settings['brand_color'], ]; } @@ -353,8 +357,14 @@ function saas_update_tenant_settings(PDO $pdo, int $tenantId, array $input): arr $pdfWatermarkText = trim((string)($input['pdf_watermark_text'] ?? '')); $pdfFooterText = trim((string)($input['pdf_footer_text'] ?? '')); $paymentReminderIntervalDays = filter_var($input['payment_reminder_interval_days'] ?? null, FILTER_VALIDATE_INT); + $brandColor = strtolower(trim((string)($input['brand_color'] ?? ''))); $errors = []; + // Leer = Standarddesign. Sonst ein vollstaendiger Hex-Wert, der ohne + // weitere Aufbereitung in das erzeugte Stylesheet geschrieben werden kann. + if ($brandColor !== '' && !preg_match('/^#[0-9a-f]{6}$/', $brandColor)) { + $errors[] = 'Die Akzentfarbe muss ein Hex-Wert wie #38761d sein.'; + } if (strlen($tenantName) < 3 || strlen($tenantName) > 255) { $errors[] = 'Der Kundenname muss zwischen 3 und 255 Zeichen lang sein.'; } @@ -433,8 +443,8 @@ function saas_update_tenant_settings(PDO $pdo, int $tenantId, array $input): arr pdf_watermark_text, pdf_footer_text, payment_reminder_enabled, payment_reminder_interval_days, cash_enabled, cash_contact, bank_transfer_enabled, - bank_account_holder, bank_iban) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + bank_account_holder, bank_iban, brand_color) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE mark_price_cents = VALUES(mark_price_cents), self_entry_enabled = VALUES(self_entry_enabled), @@ -453,7 +463,8 @@ function saas_update_tenant_settings(PDO $pdo, int $tenantId, array $input): arr cash_contact = VALUES(cash_contact), bank_transfer_enabled = VALUES(bank_transfer_enabled), bank_account_holder = VALUES(bank_account_holder), - bank_iban = VALUES(bank_iban)' + bank_iban = VALUES(bank_iban), + brand_color = VALUES(brand_color)' ); $stmt->execute([ $tenantId, @@ -475,6 +486,7 @@ function saas_update_tenant_settings(PDO $pdo, int $tenantId, array $input): arr $bankTransferEnabled, $bankAccountHolder, $bankIban, + $brandColor, ]); $pdo->commit(); diff --git a/app/tenant-logo.php b/app/tenant-logo.php index c877af8..60a2689 100644 --- a/app/tenant-logo.php +++ b/app/tenant-logo.php @@ -28,14 +28,25 @@ function tenant_logo_path(string $filename): ?string return is_file($path) ? $path : null; } +/** + * Dateiname-Praefix je Verwendungszweck. Das Wasserzeichen behaelt bewusst + * seinen historischen Praefix "logo_", damit bereits hochgeladene Dateien + * weiter gefunden werden. + */ +function tenant_logo_prefix(string $kind): string +{ + return $kind === 'brand' ? 'brand_' : 'logo_'; +} + /** * Nimmt ein hochgeladenes Logo (PNG oder JPEG) entgegen, prueft und speichert - * es unter einem festen Namen pro Mandant. + * es unter einem festen Namen pro Mandant und Verwendungszweck. * * @param array $file Eintrag aus $_FILES + * @param string $kind 'watermark' (Ausdruck) oder 'brand' (App-Oberflaeche) * @return array{ok: bool, filename?: string, error?: string} */ -function tenant_logo_store(int $tenantId, array $file): array +function tenant_logo_store(int $tenantId, array $file, string $kind = 'watermark'): array { if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) { return ['ok' => false, 'error' => 'Das Logo konnte nicht hochgeladen werden.']; @@ -63,14 +74,15 @@ function tenant_logo_store(int $tenantId, array $file): array } // Alte Varianten (andere Endung) desselben Mandanten entfernen. + $prefix = tenant_logo_prefix($kind); foreach (['png', 'jpg'] as $altExt) { - $old = $dir . '/logo_' . $tenantId . '.' . $altExt; + $old = $dir . '/' . $prefix . $tenantId . '.' . $altExt; if (is_file($old)) { @unlink($old); } } - $filename = 'logo_' . $tenantId . '.' . $ext; + $filename = $prefix . $tenantId . '.' . $ext; if (!move_uploaded_file($tmp, $dir . '/' . $filename)) { return ['ok' => false, 'error' => 'Das Logo konnte nicht gespeichert werden.']; } diff --git a/assets/css/app.css b/assets/css/app.css index 0717c9d..3654de6 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -129,6 +129,25 @@ div.hint-box { flex-direction: column; } +/* --------------------------------------------------------------------------- + * Kopfzeile mit Mandanten-Logo (headerline.php) + * + * Das Logo soll erkennbar, aber kein Blickfang sein - die Kaffeeliste bleibt + * eine Arbeitsoberflaeche. Feste Maximalhoehe, damit ein hochkantes oder sehr + * grosses Logo nicht die halbe Seite einnimmt. + * ------------------------------------------------------------------------ */ +.tenant-brand-line { + margin-bottom: 1.5em; + padding-bottom: 1em; +} + +.tenant-brand-logo { + display: block; + max-height: 60px; + max-width: 100%; + width: auto; +} + /* --------------------------------------------------------------------------- * Hervorgehobene Tabellenzeile (aktuell gebuchter Tarif) * diff --git a/backoffice-mandant.php b/backoffice-mandant.php index 987d19d..d8ffe03 100644 --- a/backoffice-mandant.php +++ b/backoffice-mandant.php @@ -2,16 +2,35 @@ require_once __DIR__ . '/functions.php'; require_once __DIR__ . '/app/platform-admin.php'; +require_once __DIR__ . '/app/features.php'; $pdo = app_db_pdo(); $user = app_require_platform_admin($pdo); -$tenantId = filter_input(INPUT_GET, 'tenant_id', FILTER_VALIDATE_INT); +// Die Mandanten-ID kommt beim Speichern der Funktions-Schalter aus dem +// Formular, sonst aus der URL. +$tenantId = filter_input(INPUT_POST, 'tenant_id', FILTER_VALIDATE_INT) + ?: filter_input(INPUT_GET, 'tenant_id', FILTER_VALIDATE_INT); if ($tenantId === null || $tenantId === false || $tenantId <= 0) { http_response_code(400); exit('Ungültige Mandanten-ID.'); } +$featureMeldung = null; +if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['aktion'] ?? '') === 'features_speichern') { + app_require_csrf(); + + $geaendert = app_features_update($pdo, $tenantId, (array)($_POST['features'] ?? [])); + if ($geaendert !== []) { + app_audit_log($pdo, $tenantId, (int)$user['user_id'], 'backoffice.features_updated', 'tenant', $tenantId, [ + 'changed' => $geaendert, + ]); + } + $featureMeldung = $geaendert === [] + ? 'Keine Änderung an den Funktionen.' + : 'Die Funktionen wurden gespeichert.'; +} + $detail = app_backoffice_fetch_tenant_detail($pdo, $tenantId); if ($detail === null) { http_response_code(404); @@ -23,6 +42,8 @@ if ($detail === null) { // nachvollziehbar bleibt (siehe AVV-Transparenzpflicht). app_audit_log($pdo, $tenantId, (int)$user['user_id'], 'backoffice.tenant_viewed', 'tenant', $tenantId); +$tenantFeatures = app_features_for_tenant($pdo, $tenantId); + $rollenLabels = ['owner' => 'Inhaber', 'admin' => 'Administrator', 'treasurer' => 'Kassenwart', 'member' => 'Mitglied', 'viewer' => 'Betrachter']; include 'header.php'; @@ -51,6 +72,39 @@ include 'nav.php'; +

Freigeschaltete Funktionen

+

Zentral vom Betreiber gesteuert: abgehakte Funktionen sind für diesen Mandanten + gesperrt und verschwinden dort aus Menü und Seiten. Die Einstellungen des Kunden + bleiben dabei erhalten.

+ + +

+ + +
+ + + +
+ $feature): ?> +
+ > + + +
+ +
+
    +
  • +
+
+

Mitglieder mit Zugang

diff --git a/csvupload.php b/csvupload.php index dc0091f..fe48122 100644 --- a/csvupload.php +++ b/csvupload.php @@ -6,6 +6,7 @@ include "functions.php"; require_once __DIR__ . "/app/ledger.php"; require_once __DIR__ . "/app/imports.php"; require_once __DIR__ . "/app/audit.php"; +require_once __DIR__ . "/app/features.php"; app_require_csrf(); include "header.php"; include "headerline.php"; @@ -36,6 +37,12 @@ if (!$hasAccess) { exit; } +if (!app_feature_enabled($pdo, $tenantId, 'csv_import')) { + echo app_feature_notice_html('csv_import'); + include "footer.php"; + exit; +} + function csv_upload_dir(): string { return APP_ROOT . '/var/uploads'; diff --git a/database/migrations/0025_tenant_features.sql b/database/migrations/0025_tenant_features.sql new file mode 100644 index 0000000..a399982 --- /dev/null +++ b/database/migrations/0025_tenant_features.sql @@ -0,0 +1,21 @@ +-- Funktions-Schalter je Mandant, gepflegt vom Betreiber im Back-Office. +-- +-- Bewusst getrennt von tenant_settings: dort stellt der Kunde selbst ein, wie +-- seine Kaffeeliste arbeitet. Hier entscheidet der Betreiber, welche +-- Funktionen dem Mandanten ueberhaupt zur Verfuegung stehen (z. B. zum +-- Abschalten einer Funktion, die bei einem Kunden Probleme macht). +-- +-- Ohne Zeile gilt eine Funktion als aktiv (siehe app/features.php); die +-- Tabelle haelt also nur die Abweichungen vom Standard. +CREATE TABLE IF NOT EXISTS tenant_features ( + id INT AUTO_INCREMENT PRIMARY KEY, + tenant_id INT NOT NULL, + feature_key VARCHAR(64) NOT NULL, + enabled TINYINT(1) NOT NULL DEFAULT 1, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY uniq_tenant_feature (tenant_id, feature_key), + CONSTRAINT fk_tenant_features_tenant + FOREIGN KEY (tenant_id) REFERENCES tenants(id) + ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/database/migrations/0026_tenant_branding.sql b/database/migrations/0026_tenant_branding.sql new file mode 100644 index 0000000..7438f1f --- /dev/null +++ b/database/migrations/0026_tenant_branding.sql @@ -0,0 +1,8 @@ +-- Eigenes Design je Mandant: Logo und Akzentfarbe fuer die App-Oberflaeche. +-- +-- Getrennt von pdf_watermark_logo: das Wasserzeichen ist ein blasses +-- Hintergrundbild fuer den Ausdruck, das Marken-Logo dagegen wird in der +-- Kopfzeile jeder Seite scharf angezeigt. Leere Werte = Standarddesign. +ALTER TABLE tenant_settings + ADD COLUMN brand_logo VARCHAR(255) NOT NULL DEFAULT '' AFTER bank_iban, + ADD COLUMN brand_color VARCHAR(7) NOT NULL DEFAULT '' AFTER brand_logo; diff --git a/datenexport.php b/datenexport.php index 892e38b..0060118 100644 --- a/datenexport.php +++ b/datenexport.php @@ -3,6 +3,7 @@ require_once __DIR__ . '/functions.php'; require_once __DIR__ . '/app/data-export.php'; require_once __DIR__ . '/app/audit.php'; +require_once __DIR__ . '/app/features.php'; $pdo = app_db_pdo(); $user = saas_require_login(); @@ -12,6 +13,11 @@ if (!saas_can_manage_tenant_settings($user)) { exit('Kein Zugriff.'); } +if (!app_feature_enabled($pdo, (int)$user['tenant_id'], 'data_export')) { + http_response_code(403); + exit('Der Datenexport ist für euren Zugang nicht freigeschaltet.'); +} + if ($_SERVER['REQUEST_METHOD'] === 'POST') { app_require_csrf(); diff --git a/docs/backlog-druck-und-landingpage.md b/docs/backlog-druck-und-landingpage.md index 9e113e5..532453e 100644 --- a/docs/backlog-druck-und-landingpage.md +++ b/docs/backlog-druck-und-landingpage.md @@ -29,8 +29,13 @@ dort ist ab zwei Mitgliedschaften auch „Mandant wechseln" ergänzt. - ✅ Adaptives Layout nach Mitgliederanzahl (Zeilenhöhe 16–40px je nach Mitgliederzahl). -- ✅ Vorder-/Rückseite bzw. mehrere Seiten erst ab 50 Mitgliedern - (`EXPORT_MULTI_PAGE_THRESHOLD`), darunter eine einseitige Liste. +- ✅ Vorder-/Rückseite bzw. mehrere Seiten erst bei **mehr als** 50 + Mitgliedern, bis einschließlich 50 eine einseitige Liste. Die Schwelle + liegt seit 2026-08-17 als `LEDGER_SINGLE_PAGE_MAX_MEMBERS` / + `ledger_uses_two_sided_sheet()` in `app/ledger.php` und gilt damit + einheitlich für den Ausdruck **und** die Vorder-/Rückseiten-Auswahl beim + Erfassen (`stricheintragen.php`, `einzahlung.php`) – dort erschienen die + Buttons vorher auch bei Kleinstteams, für die es nur ein Blatt gibt. - ✅ Admin-Einstellung „Freie Zeilen für neue Mitglieder" (`tenant_settings.pdf_show_empty_rows`, Formular in `mandant-einstellungen.php`). diff --git a/docs/backoffice.md b/docs/backoffice.md index af84bfd..975cee8 100644 --- a/docs/backoffice.md +++ b/docs/backoffice.md @@ -54,15 +54,75 @@ demselben Grund wie beim Setzen). - `backoffice.php`: Übersicht aller Mandanten mit Kürzel, Status, Erstelldatum, Teilnehmerzahl (aktiv/gesamt) und Saldensumme. -- `backoffice-mandant.php?tenant_id=X`: reine Leseansicht eines einzelnen - Mandanten – Einstellungen, Mitglieder mit Rolle, letzte 20 Buchungen, - letzte 20 Admin-Aktionen. Absichtlich **kein** Schreibzugriff auf - Mandantendaten von hier aus, um das Risiko einer versehentlichen - Fremdänderung auszuschließen. +- `backoffice-mandant.php?tenant_id=X`: Ansicht eines einzelnen Mandanten + – Einstellungen, Mitglieder mit Rolle, letzte 20 Buchungen, letzte 20 + Admin-Aktionen, dazu die Funktions-Schalter (siehe unten). Absichtlich + **kein** Schreibzugriff auf die eigentlichen Mandantendaten (Buchungen, + Mitglieder, Einstellungen des Kunden) von hier aus, um das Risiko einer + versehentlichen Fremdänderung auszuschließen. - `backoffice-export.php`: nutzt dieselbe `app_export_tenant_data()`- Funktion wie der Selbstbedienungs-Export, aber ausgelöst durch den Platform-Admin für einen beliebigen Mandanten. +## Funktions-Schalter je Mandant + +Stand: 2026-08-17. Zentralstelle des Betreibers, um einzelne Funktionen +pro Mandant freizuschalten oder zu sperren – gepflegt in +`backoffice-mandant.php`, umgesetzt in `app/features.php` und der Tabelle +`tenant_features` (Migration `0025_tenant_features.sql`). + +Schaltbar sind: FAQ-Seite, Striche selbst eintragen, Kaffeeliste als PDF, +PayPal-Zahlungseingang, CSV-Import, Info-Mails und Erinnerungen, +Jahresabschluss, Datenexport, eigenes Design. + +Design-Entscheidungen: + +- **Getrennt von `tenant_settings`.** Dort stellt der Kunde ein, *wie* + eine Funktion arbeitet; hier entscheidet der Betreiber, ob sie ihm + überhaupt zur Verfügung steht. Beide Prüfungen stehen nebeneinander, + z. B. beim Selbsteintrag: `self_entry_enabled` (Kunde) **und** + Feature `self_entry` (Betreiber). +- **Ohne Zeile = freigeschaltet.** `tenant_features` hält nur die + Abweichungen vom Standard; ein neuer Mandant bekommt automatisch den + vollen Funktionsumfang, ohne dass der Betreiber etwas einschalten muss. + Fehlt die Tabelle (Migration noch nicht ausgerollt), bleibt ebenfalls + alles freigeschaltet – eine ausstehende Migration darf keine Funktion + sperren. +- **Gesperrt heißt unsichtbar.** Die betroffenen Menüpunkte und + Schaltflächen verschwinden (`footer.php`, `kaffeeliste.php`, + `einzahlung.php`, `konto.php`); ein direkter Aufruf der Seite endet mit + 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. +- Jede Änderung landet als `backoffice.features_updated` im Audit-Log des + betroffenen Mandanten – wie jeder andere Back-Office-Zugriff auch. + +Das Back-Office ist außerdem jetzt für Platform-Admins im Menü verlinkt +(Gruppe „Betreiber" in `footer.php`); vorher war es nur über die direkte +URL erreichbar. + +## Eigenes Design je Mandant + +Stand: 2026-08-17, hängt am Feature `branding`. Der Kunde hinterlegt in +`mandant-einstellungen.php` eine Akzentfarbe und ein Logo: + +- Die Farbe (`tenant_settings.brand_color`, validiert als `#rrggbb`) wird + in `header.php` als kleines Stylesheet eingebettet + (`app/branding.php`), das die Akzentfarbe des Templates überschreibt. + Eine eigene CSS-Datei je Mandant wäre ein zusätzlicher Request für + wenige Zeilen. +- Das Logo (`tenant_settings.brand_logo`) liegt wie das PDF-Wasserzeichen + in `var/tenant_logos` und ist damit **nicht** direkt per URL abrufbar – + sonst wäre jedes Kundenlogo unter einer ratbaren Adresse öffentlich. + Ausgeliefert wird es über `tenant-logo-anzeigen.php`, das ausschließlich + das Logo des Mandanten des angemeldeten Nutzers ausgibt. +- Wasserzeichen (Ausdruck) und Marken-Logo (Oberfläche) sind getrennte + Dateien mit eigenem Namenspräfix (`logo_` bzw. `brand_`). +- Sperrt der Betreiber `branding`, verschwindet der Abschnitt aus den + Einstellungen und die Oberfläche fällt aufs Standarddesign zurück. Die + gespeicherte Farbe bleibt erhalten und gilt nach einer erneuten + Freischaltung wieder. + ## Verhältnis zum Selbstbedienungs-Export (`datenexport.php`) Bewusste Entscheidung: Der bestehende Selbstbedienungs-Export für diff --git a/einzahlung.php b/einzahlung.php index 6c0fffd..7f0d1ad 100644 --- a/einzahlung.php +++ b/einzahlung.php @@ -4,6 +4,7 @@ ob_start(); include "functions.php"; require_once __DIR__ . "/app/ledger.php"; +require_once __DIR__ . "/app/features.php"; app_require_csrf(); include "header.php"; @@ -193,7 +194,17 @@ if ($_SERVER["REQUEST_METHOD"] == "POST" ) { $action = (string)($_GET['action'] ?? 'alle'); } -$mitarbeiter = einzahlung_fetch_participants($pdo, $tenantId, $action); +// Vorder-/Rueckseite nur, wenn der Ausdruck auch wirklich zwei Seiten hat +// (siehe ledger_uses_two_sided_sheet). Bei kleinen Teams gibt es nur ein Blatt. +$alleAktiven = ledger_fetch_participant_summaries($pdo, $tenantId, ['active_only' => true]); +$zweiseitig = ledger_uses_two_sided_sheet(count($alleAktiven)); +if (!$zweiseitig) { + $action = 'alle'; +} + +$mitarbeiter = $action === 'alle' + ? $alleAktiven + : einzahlung_fetch_participants($pdo, $tenantId, $action); ?> @@ -223,6 +234,7 @@ $mitarbeiter = einzahlung_fetch_participants($pdo, $tenantId, $action);
    +
  • @@ -235,6 +247,7 @@ $mitarbeiter = einzahlung_fetch_participants($pdo, $tenantId, $action);
  • +
  • @@ -246,11 +259,13 @@ $mitarbeiter = einzahlung_fetch_participants($pdo, $tenantId, $action);
  • +
  • +
"> diff --git a/exportKaffeeliste.php b/exportKaffeeliste.php index 71aa1b0..e79aaba 100644 --- a/exportKaffeeliste.php +++ b/exportKaffeeliste.php @@ -3,6 +3,7 @@ include "functions.php"; require_once __DIR__ . "/app/ledger.php"; require_once __DIR__ . "/app/tenant-logo.php"; +require_once __DIR__ . "/app/features.php"; $pdo = app_db_pdo(); $saasUser = saas_current_user($pdo); @@ -19,6 +20,11 @@ if (!$hasAccess) { exit('Kein Zugriff.'); } +if (!app_feature_enabled($pdo, $tenantId, 'pdf_export')) { + http_response_code(403); + exit('Der Ausdruck der Kaffeeliste ist für euren Zugang nicht freigeschaltet.'); +} + $settings = saas_fetch_tenant_settings($pdo, $tenantId); $windowDays = $settings['sheet_window_days'] ?? 100; $markPrice = number_format(($settings['mark_price_cents'] ?? 20) / 100, 2, ',', '.'); @@ -29,9 +35,9 @@ $watermarkText = trim((string)($settings['pdf_watermark_text'] ?? '')); $footerText = trim((string)($settings['pdf_footer_text'] ?? '')); $watermarkLogoPath = tenant_logo_path((string)($settings['pdf_watermark_logo'] ?? '')) ?? ''; -// Ab dieser Mitgliederzahl wird auf zwei Seiten (Vorder-/Rueckseite) statt -// einer einzelnen Seite gedruckt. -define('EXPORT_MULTI_PAGE_THRESHOLD', 50); +// Vorder-/Rueckseite erst oberhalb der gemeinsamen Schwelle aus app/ledger.php +// (LEDGER_SINGLE_PAGE_MAX_MEMBERS): bis einschliesslich 50 Personen bleibt der +// Ausdruck bewusst einseitig. // TCPDF's writeHTML() ignoriert CSS-Zeilenhoehen/-innenabstaende auf // Tabellenzeilen vollstaendig (getestet: weder "height" auf noch // "padding" auf
NameE-MailRolleStatus
haben irgendeinen Einfluss auf die gerenderte Zeilen- @@ -57,7 +63,7 @@ define('EXPORT_ROW_OVERHEAD_MM', 0.2); $allActive = ledger_fetch_participant_summaries($pdo, $tenantId, ['active_only' => true]); $memberCount = count($allActive); -$isMultiPage = $memberCount >= EXPORT_MULTI_PAGE_THRESHOLD; +$isMultiPage = ledger_uses_two_sided_sheet($memberCount); /** * Verfuegbare Hoehe (mm) fuer die Datenzeilen einer A4-Seite: Gesamthoehe diff --git a/faq.php b/faq.php index c56f892..cfa93bd 100644 --- a/faq.php +++ b/faq.php @@ -4,6 +4,7 @@ include "functions.php"; require_once __DIR__ . "/app/ledger.php"; require_once __DIR__ . "/app/faq.php"; require_once __DIR__ . "/app/audit.php"; +require_once __DIR__ . "/app/features.php"; app_require_csrf(); include "header.php"; include "headerline.php"; @@ -37,6 +38,12 @@ if (!$hasAccess) { exit; } +if (!app_feature_enabled($pdo, $tenantId, 'faq')) { + echo app_feature_notice_html('faq'); + include "footer.php"; + exit; +} + $meldung = null; $fehler = null; $bearbeitenId = null; diff --git a/footer.php b/footer.php index 1cd1dc9..305f208 100644 --- a/footer.php +++ b/footer.php @@ -30,6 +30,31 @@ if ($saasNavUser !== null && function_exists('saas_list_user_memberships')) { $saasHasMultipleTenants = false; } } + +// Vom Betreiber gesperrte Funktionen erscheinen gar nicht erst im Menue - +// ein Menuepunkt, der nur zu einer Absage fuehrt, ist schlechter als keiner. +require_once __DIR__ . '/app/features.php'; +$saasNavTenantId = $saasNavUser !== null ? (int)$saasNavUser['tenant_id'] : 0; +$saasNavFeature = static function (string $key) use ($saasNavTenantId): bool { + if ($saasNavTenantId <= 0) { + return true; + } + + return app_feature_enabled(app_db_pdo(), $saasNavTenantId, $key); +}; + +// Das Back-Office war bisher nur ueber die direkte URL erreichbar. Es ist die +// Zentralstelle des Betreibers und gehoert deshalb ins Menue - aber nur fuer +// Platform-Admins, nicht fuer Mandanten-Rollen. +$saasNavIsPlatformAdmin = false; +if ($saasNavUser !== null) { + try { + require_once __DIR__ . '/app/platform-admin.php'; + $saasNavIsPlatformAdmin = app_is_platform_admin(app_db_pdo(), (int)$saasNavUser['user_id']); + } catch (Throwable $e) { + $saasNavIsPlatformAdmin = false; + } +} ?> diff --git a/header.php b/header.php index 0744f59..cbe584d 100644 --- a/header.php +++ b/header.php @@ -11,12 +11,27 @@ - - \n" . app_branding_css($headerBranding['color']) . "\t\t\n"; +} + +?> + + +' + . '' + . ''; +} diff --git a/index.php b/index.php index dedbe91..0cd9076 100644 --- a/index.php +++ b/index.php @@ -15,6 +15,7 @@ if ($appHost !== null && $requestHost !== strtolower($appHost)) { include "functions.php"; require_once __DIR__ . "/app/ledger.php"; +require_once __DIR__ . "/app/features.php"; app_require_csrf(); $pdo = app_db_pdo(); @@ -106,6 +107,12 @@ function dashboard_record_self_entry(PDO $pdo, array $participant, array $settin return ['ok' => false, 'error' => 'Die eigene Stricherfassung ist aktuell deaktiviert.']; } + // Zweite, uebergeordnete Huerde: der Betreiber kann die Funktion fuer den + // ganzen Mandanten sperren, unabhaengig von dessen eigener Einstellung. + if (!app_feature_enabled($pdo, (int)$participant['tenant_id'], 'self_entry')) { + return ['ok' => false, 'error' => 'Die eigene Stricherfassung ist für euren Zugang nicht freigeschaltet.']; + } + $marks = filter_var($marksValue, FILTER_VALIDATE_INT); if ($marks !== 1 && $marks !== 2) { return ['ok' => false, 'error' => 'Bitte einen oder zwei Striche auswählen.']; @@ -223,7 +230,8 @@ if ($hasAccess && $participant !== null && $settings !== null) { $balanceCents = (int)$participant['balance_cents']; $balance = dashboard_money($balanceCents); $currentName = dashboard_current_name($saasUser, $participant); - $selfEntryEnabled = (int)$settings['self_entry_enabled'] === 1; + $selfEntryEnabled = (int)$settings['self_entry_enabled'] === 1 + && app_feature_enabled($pdo, $tenantId, 'self_entry'); $paypalEnabled = (int)$settings['paypal_enabled'] === 1 && trim((string)$settings['paypal_url_template']) !== ''; $cashEnabled = (int)$settings['cash_enabled'] === 1; diff --git a/jahresauswertung.php b/jahresauswertung.php index 57cdee7..e90e498 100644 --- a/jahresauswertung.php +++ b/jahresauswertung.php @@ -4,6 +4,7 @@ include "functions.php"; require_once __DIR__ . "/app/ledger.php"; require_once __DIR__ . "/app/saas-mail.php"; require_once __DIR__ . "/app/audit.php"; +require_once __DIR__ . "/app/features.php"; app_require_csrf(); $pdo = app_db_pdo(); @@ -16,6 +17,14 @@ 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; +if ($hasAccess && !app_feature_enabled($pdo, $tenantId, 'year_closing')) { + $gesperrteFunktion = 'year_closing'; + $hasAccess = false; +} + $jahr = (int)date('Y'); $fehler = null; $ergebnisse = null; @@ -181,7 +190,9 @@ include "nav.php";


+
+ Kein Zugriff"; diff --git a/konto.php b/konto.php index 5a7fef0..5b1985f 100644 --- a/konto.php +++ b/konto.php @@ -1,6 +1,7 @@
  • Mandant-Einstellungen
  • -
  • Datenexport
  • + +
  • Datenexport
  • +
  • Mandant löschen
  • diff --git a/mailversenden.php b/mailversenden.php index 0ccc8fc..a189168 100644 --- a/mailversenden.php +++ b/mailversenden.php @@ -4,6 +4,7 @@ include "functions.php"; require_once __DIR__ . "/app/ledger.php"; require_once __DIR__ . "/app/saas-mail.php"; require_once __DIR__ . "/app/audit.php"; +require_once __DIR__ . "/app/features.php"; app_require_csrf(); $pdo = app_db_pdo(); @@ -16,6 +17,14 @@ 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; +if ($hasAccess && !app_feature_enabled($pdo, $tenantId, 'mail_dispatch')) { + $gesperrteFunktion = 'mail_dispatch'; + $hasAccess = false; +} + $ergebnisse = null; $dryRun = true; $liveFlash = false; @@ -117,7 +126,9 @@ include "nav.php";