Files
kaffeekasse-saas/app/branding.php
T
clemensandClaude Opus 5 d24ee9bf91 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>
2026-08-17 22:52:55 +02:00

106 lines
3.9 KiB
PHP

<?php
declare(strict_types=1);
require_once __DIR__ . '/bootstrap.php';
require_once __DIR__ . '/saas-auth.php';
require_once __DIR__ . '/features.php';
/**
* Logo und Akzentfarbe des Mandanten, an dem der angemeldete Nutzer haengt.
* Ohne Anmeldung, ohne freigeschaltete Funktion „Eigenes Design" oder ohne
* hinterlegte Werte kommt ueberall ein leerer String zurueck - der Aufrufer
* bleibt dann beim Standarddesign.
*
* Das Ergebnis wird pro Request gecached: header.php laeuft auf jeder Seite
* und soll dafuer nicht mehrfach die Einstellungen laden.
*
* @return array{color: string, logo_url: string, tenant_name: string}
*/
function app_branding_for_current_user(PDO $pdo): array
{
static $cached = null;
if ($cached !== null) {
return $cached;
}
$leer = ['color' => '', '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 <<<CSS
:root { --brand-color: {$c}; }
a { color: var(--brand-color); }
a:hover { border-bottom-color: var(--brand-color); color: var(--brand-color) !important; }
header.major > :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;
}