Wasserzeichen fuer den kostenlosen Tarif
Mandanten im Tarif "free" (bis 10 Teilnehmer) zeigen unten rechts eine kleine Marke mit Logo und Adresse der Kaffeeliste; zahlende Mandanten behalten ihre Oberflaeche ohne fremdes Logo. Die Sichtbarkeit haengt allein an tenant_billing.plan_code und wird als reine Leseabfrage geprueft - billing_fetch_or_init() waere ein Schreibzugriff im Seitenaufbau. Laesst sich der Tarif nicht lesen, bleibt die Marke aus. Das Logo ist ein Inline-SVG, das Linkziel kommt aus APP_MARKETING_URL bzw. aus APP_HOST ohne App-Subdomain. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -99,6 +99,33 @@ function app_primary_url(string $path): string
|
|||||||
return $scheme . '://' . $host . '/' . ltrim($path, '/');
|
return $scheme . '://' . $host . '/' . ltrim($path, '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adresse der Marketing-Domain (z. B. https://kaffeeliste.de) - Ziel des
|
||||||
|
* Wasserzeichens im kostenlosen Tarif. Explizit ueber APP_MARKETING_URL
|
||||||
|
* setzbar; sonst wird sie aus APP_HOST abgeleitet, indem die
|
||||||
|
* App-Subdomain wegfaellt (app.kaffeeliste.de -> kaffeeliste.de). In der
|
||||||
|
* lokalen Entwicklung ohne APP_HOST bleibt der aktuelle Host uebrig, im
|
||||||
|
* CLI-Betrieb ein leerer String.
|
||||||
|
*/
|
||||||
|
function app_marketing_url(): string
|
||||||
|
{
|
||||||
|
$configured = app_env('APP_MARKETING_URL');
|
||||||
|
if ($configured !== null) {
|
||||||
|
return rtrim($configured, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
$scheme = app_is_https() ? 'https' : 'http';
|
||||||
|
$host = app_primary_host();
|
||||||
|
|
||||||
|
if ($host !== null) {
|
||||||
|
return $scheme . '://' . preg_replace('/^app\./i', '', $host);
|
||||||
|
}
|
||||||
|
|
||||||
|
$requestHost = (string)($_SERVER['HTTP_HOST'] ?? '');
|
||||||
|
|
||||||
|
return $requestHost !== '' ? $scheme . '://' . $requestHost : '';
|
||||||
|
}
|
||||||
|
|
||||||
function app_start_session(): void
|
function app_start_session(): void
|
||||||
{
|
{
|
||||||
if (PHP_SAPI === 'cli' || session_status() === PHP_SESSION_ACTIVE) {
|
if (PHP_SAPI === 'cli' || session_status() === PHP_SESSION_ACTIVE) {
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/bootstrap.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wasserzeichen des kostenlosen Tarifs: eine kleine Marke unten rechts, die
|
||||||
|
* auf die Kaffeeliste verweist. Sie erscheint nur im Tarif "free" - wer
|
||||||
|
* zahlt, bekommt seine Oberflaeche ohne fremdes Logo.
|
||||||
|
*
|
||||||
|
* Bewusst getrennt von app/branding.php: dort geht es um das Logo *des
|
||||||
|
* Mandanten*, hier um das Logo *des Anbieters*. Beide koennen gleichzeitig
|
||||||
|
* sichtbar sein (Kopfzeile oben, Wasserzeichen unten rechts).
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tarif eines Mandanten, ohne Seiteneffekt. Absichtlich nicht
|
||||||
|
* billing_fetch_or_init(): das Wasserzeichen wird auf jeder Seite geprueft
|
||||||
|
* und darf dabei keine Zeile anlegen. Fehlt der Datensatz, gilt - wie beim
|
||||||
|
* Anlegen - "free".
|
||||||
|
*/
|
||||||
|
function plan_watermark_plan_code(PDO $pdo, int $tenantId): string
|
||||||
|
{
|
||||||
|
static $cache = [];
|
||||||
|
|
||||||
|
if (array_key_exists($tenantId, $cache)) {
|
||||||
|
return $cache[$tenantId];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare('SELECT plan_code FROM tenant_billing WHERE tenant_id = ?');
|
||||||
|
$stmt->execute([$tenantId]);
|
||||||
|
$planCode = $stmt->fetchColumn();
|
||||||
|
$cache[$tenantId] = $planCode === false ? 'free' : (string)$planCode;
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
// Laesst sich der Tarif nicht lesen, bleibt die Oberflaeche sauber -
|
||||||
|
// ein Wasserzeichen bei einem zahlenden Kunden waere der schlimmere
|
||||||
|
// Fehler als ein fehlendes bei einem kostenlosen.
|
||||||
|
$cache[$tenantId] = 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $cache[$tenantId];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zeigt dieser Mandant das Wasserzeichen? Nur der kostenlose Tarif tut das.
|
||||||
|
*/
|
||||||
|
function plan_watermark_visible(PDO $pdo, int $tenantId): bool
|
||||||
|
{
|
||||||
|
if ($tenantId <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return plan_watermark_plan_code($pdo, $tenantId) === 'free';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wortmarke der Kaffeeliste als Inline-SVG. Als Inline-Grafik statt als
|
||||||
|
* Bilddatei, damit das Wasserzeichen ohne zusaetzlichen Request auskommt,
|
||||||
|
* auf jedem Bildschirm scharf bleibt und die Farbe der Umgebung uebernimmt.
|
||||||
|
*/
|
||||||
|
function plan_watermark_logo_svg(): string
|
||||||
|
{
|
||||||
|
return '<svg class="plan-watermark-logo" viewBox="0 0 24 24" width="18" height="18" '
|
||||||
|
. 'aria-hidden="true" focusable="false" fill="none" stroke="currentColor" '
|
||||||
|
. 'stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">'
|
||||||
|
. '<path d="M4 9h13v6a4 4 0 0 1-4 4H8a4 4 0 0 1-4-4V9Z"/>'
|
||||||
|
. '<path d="M17 10.5h1.5a2.5 2.5 0 0 1 0 5H17"/>'
|
||||||
|
. '<path d="M8 5.5c0-1 1-1.4 1-2.5M12 5.5c0-1 1-1.4 1-2.5"/>'
|
||||||
|
. '</svg>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Markup des Wasserzeichens. Ohne erreichbare Marketing-Domain (CLI,
|
||||||
|
* ungewoehnliche Hosts) bleibt es bei Logo und Namen ohne Link - ein
|
||||||
|
* Verweis ins Leere waere schlechter als keiner.
|
||||||
|
*/
|
||||||
|
function plan_watermark_html(): string
|
||||||
|
{
|
||||||
|
$url = app_marketing_url();
|
||||||
|
$label = $url !== '' ? preg_replace('#^https?://#i', '', $url) : 'Kaffeeliste';
|
||||||
|
$inner = plan_watermark_logo_svg()
|
||||||
|
. '<span class="plan-watermark-text">' . htmlspecialchars($label, ENT_QUOTES, 'UTF-8') . '</span>';
|
||||||
|
|
||||||
|
if ($url === '') {
|
||||||
|
return '<div class="plan-watermark" title="Kostenloser Tarif der Kaffeeliste">' . $inner . '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '<a class="plan-watermark" href="' . htmlspecialchars($url, ENT_QUOTES, 'UTF-8') . '"'
|
||||||
|
. ' target="_blank" rel="noopener noreferrer"'
|
||||||
|
. ' title="Diese Kaffeeliste läuft im kostenlosen Tarif">' . $inner . '</a>';
|
||||||
|
}
|
||||||
@@ -163,3 +163,55 @@ tr.tarif-aktuell > td {
|
|||||||
tr.tarif-aktuell > td:first-child {
|
tr.tarif-aktuell > td:first-child {
|
||||||
box-shadow: inset 5px 0 0 0 #4caf50;
|
box-shadow: inset 5px 0 0 0 #4caf50;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------------
|
||||||
|
* Wasserzeichen im kostenlosen Tarif (app/plan-watermark.php)
|
||||||
|
*
|
||||||
|
* Fest unten rechts, damit es unabhaengig von der Seitenlaenge sichtbar
|
||||||
|
* bleibt. Zurueckhaltend gehalten: die Kaffeeliste ist eine Arbeitsflaeche,
|
||||||
|
* der Hinweis soll erkennbar sein, aber nichts verdecken - deshalb halb
|
||||||
|
* transparent, erst beim Darueberfahren voll sichtbar.
|
||||||
|
* ------------------------------------------------------------------------ */
|
||||||
|
.plan-watermark {
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
|
border-radius: 999px;
|
||||||
|
bottom: 0.9rem;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
|
||||||
|
color: #4a4a4a;
|
||||||
|
display: inline-flex;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
gap: 0.4em;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
line-height: 1;
|
||||||
|
opacity: 0.65;
|
||||||
|
padding: 0.45em 0.8em;
|
||||||
|
position: fixed;
|
||||||
|
right: 0.9rem;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
z-index: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.plan-watermark {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-watermark:hover,
|
||||||
|
.plan-watermark:focus {
|
||||||
|
color: #4a4a4a;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-watermark-logo {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Auf schmalen Bildschirmen bleibt nur das Logo - der Schriftzug wuerde dort
|
||||||
|
ueber den Inhalt laufen. */
|
||||||
|
@media screen and (max-width: 480px) {
|
||||||
|
.plan-watermark-text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -327,3 +327,37 @@ mandant-einstellungen.php (vollständige Tarif-Vergleichstabelle mit Bestell-But
|
|||||||
Zugriff wurden hier durchgeführt. Vor Go-Live einmal im Stripe-Testmodus
|
Zugriff wurden hier durchgeführt. Vor Go-Live einmal im Stripe-Testmodus
|
||||||
gegen einen echten Test-Mandanten durchspielen (Upgrade, Downgrade
|
gegen einen echten Test-Mandanten durchspielen (Upgrade, Downgrade
|
||||||
zwischen zwei bezahlten Stufen, Downgrade auf `free`).
|
zwischen zwei bezahlten Stufen, Downgrade auf `free`).
|
||||||
|
|
||||||
|
## Wasserzeichen im kostenlosen Tarif (umgesetzt)
|
||||||
|
|
||||||
|
Stand: 2026-08-21. Mandanten im Tarif `free` (bis 10 Teilnehmer) zeigen
|
||||||
|
unten rechts eine kleine Marke mit Logo und Adresse der Kaffeeliste.
|
||||||
|
Zahlende Mandanten sehen die Oberfläche ohne fremdes Logo.
|
||||||
|
|
||||||
|
Umgesetzte Dateien:
|
||||||
|
|
||||||
|
```text
|
||||||
|
app/plan-watermark.php (Sichtbarkeit + Markup)
|
||||||
|
app/bootstrap.php (app_marketing_url())
|
||||||
|
footer.php (Einbindung vor den Skripten)
|
||||||
|
assets/css/app.css (.plan-watermark)
|
||||||
|
scripts/http-smoke.php (Dashboard-Check)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Nur der Tarif entscheidet.** `plan_watermark_visible()` blendet die
|
||||||
|
Marke ein, wenn `tenant_billing.plan_code` `free` ist – fehlt der
|
||||||
|
Datensatz, gilt wie beim Anlegen `free`. Lässt sich der Tarif nicht
|
||||||
|
lesen, bleibt die Oberfläche sauber: ein Wasserzeichen bei einem
|
||||||
|
zahlenden Kunden wäre der schlimmere Fehler.
|
||||||
|
- **Keine Schreibzugriffe im Seitenaufbau.** Bewusst nicht
|
||||||
|
`billing_fetch_or_init()`, das eine fehlende Zeile anlegen würde; die
|
||||||
|
Prüfung ist eine reine Leseabfrage und wird pro Request zwischengespeichert.
|
||||||
|
- **Logo als Inline-SVG.** Spart einen Request, bleibt auf jedem Bildschirm
|
||||||
|
scharf und übernimmt die Farbe der Umgebung. Ein eigenes Bild lässt sich
|
||||||
|
in `plan_watermark_logo_svg()` durch ein `<img>` ersetzen.
|
||||||
|
- **Ziel der Verlinkung** ist die Marketing-Domain: `APP_MARKETING_URL`,
|
||||||
|
ersatzweise `APP_HOST` ohne die App-Subdomain
|
||||||
|
(`app.kaffeeliste.de` → `kaffeeliste.de`).
|
||||||
|
- Getrennt von `app/branding.php`: dort geht es um das Logo *des Mandanten*
|
||||||
|
(Kopfzeile), hier um das *des Anbieters*. Beides kann gleichzeitig
|
||||||
|
sichtbar sein.
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ putenv('APP_TIMEZONE=Europe/Berlin');
|
|||||||
// stattdessen die Landingpage. Siehe index.php.
|
// stattdessen die Landingpage. Siehe index.php.
|
||||||
putenv('APP_HOST=app.kaffeeliste.de');
|
putenv('APP_HOST=app.kaffeeliste.de');
|
||||||
|
|
||||||
|
// Ziel des Wasserzeichens im kostenlosen Tarif (unten rechts in der App).
|
||||||
|
// Ohne Angabe wird die Marketing-Domain aus APP_HOST abgeleitet, indem die
|
||||||
|
// App-Subdomain wegfaellt - hier also https://kaffeeliste.de.
|
||||||
|
putenv('APP_MARKETING_URL=https://kaffeeliste.de');
|
||||||
|
|
||||||
putenv('APP_DB_DRIVER=mysql');
|
putenv('APP_DB_DRIVER=mysql');
|
||||||
putenv('DB_HOST=');
|
putenv('DB_HOST=');
|
||||||
putenv('DB_PORT=3306');
|
putenv('DB_PORT=3306');
|
||||||
|
|||||||
+11
@@ -142,6 +142,17 @@ if ($saasNavUser !== null) {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// Wasserzeichen des kostenlosen Tarifs: kleine Marke unten rechts, die auf
|
||||||
|
// die Kaffeeliste verweist. Zahlende Mandanten sehen sie nicht.
|
||||||
|
if ($saasNavTenantId > 0) {
|
||||||
|
require_once __DIR__ . '/app/plan-watermark.php';
|
||||||
|
if (plan_watermark_visible(app_db_pdo(), $saasNavTenantId)) {
|
||||||
|
echo plan_watermark_html();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
<script src="assets/js/jquery.min.js"></script>
|
<script src="assets/js/jquery.min.js"></script>
|
||||||
<script src="assets/js/browser.min.js"></script>
|
<script src="assets/js/browser.min.js"></script>
|
||||||
|
|||||||
@@ -52,7 +52,9 @@ $checks = [
|
|||||||
'label' => 'Dashboard',
|
'label' => 'Dashboard',
|
||||||
'path' => 'index.php',
|
'path' => 'index.php',
|
||||||
'auth' => true,
|
'auth' => true,
|
||||||
'contains' => ['Aktueller Stand', 'Jahresübersicht', 'Letzte Einzahlungen'],
|
// Der Smoke-Mandant laeuft im kostenlosen Tarif, also muss das
|
||||||
|
// Wasserzeichen unten rechts mitkommen.
|
||||||
|
'contains' => ['Aktueller Stand', 'Jahresübersicht', 'Letzte Einzahlungen', 'class="plan-watermark"'],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'label' => 'Striche erfassen',
|
'label' => 'Striche erfassen',
|
||||||
|
|||||||
Reference in New Issue
Block a user