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,105 @@
|
||||
<?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;
|
||||
}
|
||||
+7
-8
@@ -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 (?, ?, ?, ?)');
|
||||
|
||||
@@ -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>';
|
||||
}
|
||||
@@ -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
|
||||
|
||||
+16
-4
@@ -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();
|
||||
|
||||
+16
-4
@@ -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.'];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user