Schalter fuer den Hinweis auf die Kaffeeliste
Der Hinweis unten rechts laesst sich ab einem bezahlten Tarif in den Mandant-Einstellungen abschalten (neue Spalte tenant_settings. watermark_enabled, Standard an). Im kostenlosen Tarif bleibt er fest eingeschaltet: das Kaestchen ist angehakt und disabled, daneben steht der Grund. Zusaetzlich erzwingt saas_update_tenant_settings() den Wert dort noch einmal, damit ein nachgebauter POST ihn nicht abschalten kann. Der Titel des Wasserzeichens spricht nicht mehr vom kostenlosen Tarif - zahlende Mandanten koennen es jetzt freiwillig zeigen. Der Smoke-Test prueft die Einstellungsseite jetzt auch angemeldet. Sein Muster fuer PHP-Meldungen verlangt dafuer einen Doppelpunkt, sonst haette das Feld "negative_warning" als Warnung gezaehlt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+18
-4
@@ -44,7 +44,10 @@ function plan_watermark_plan_code(PDO $pdo, int $tenantId): string
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt dieser Mandant das Wasserzeichen? Nur der kostenlose Tarif tut das.
|
||||
* Zeigt dieser Mandant das Wasserzeichen? Im kostenlosen Tarif immer - es ist
|
||||
* der Gegenwert fuer die kostenlose Nutzung und deshalb nicht abschaltbar.
|
||||
* Alle anderen entscheiden ueber die Mandant-Einstellung
|
||||
* `watermark_enabled` (Standard: an).
|
||||
*/
|
||||
function plan_watermark_visible(PDO $pdo, int $tenantId): bool
|
||||
{
|
||||
@@ -52,7 +55,18 @@ function plan_watermark_visible(PDO $pdo, int $tenantId): bool
|
||||
return false;
|
||||
}
|
||||
|
||||
return plan_watermark_plan_code($pdo, $tenantId) === 'free';
|
||||
$planCode = plan_watermark_plan_code($pdo, $tenantId);
|
||||
if ($planCode === 'free') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($planCode === 'unknown') {
|
||||
return false;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/features.php';
|
||||
|
||||
return app_tenant_switch_enabled($pdo, $tenantId, 'watermark_enabled');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,10 +98,10 @@ function plan_watermark_html(): string
|
||||
. '<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 '<div class="plan-watermark" title="Kaffeeliste – die digitale Kaffeekasse">' . $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>';
|
||||
. ' title="Kaffeeliste – die digitale Kaffeekasse">' . $inner . '</a>';
|
||||
}
|
||||
|
||||
+18
-4
@@ -288,7 +288,8 @@ function saas_fetch_tenant_settings(PDO $pdo, int $tenantId): ?array
|
||||
ts.bank_account_holder,
|
||||
ts.bank_iban,
|
||||
ts.brand_logo,
|
||||
ts.brand_color
|
||||
ts.brand_color,
|
||||
ts.watermark_enabled
|
||||
FROM tenants t
|
||||
LEFT JOIN tenant_settings ts ON ts.tenant_id = t.id
|
||||
WHERE t.id = ?
|
||||
@@ -336,6 +337,7 @@ function saas_fetch_tenant_settings(PDO $pdo, int $tenantId): ?array
|
||||
'bank_iban' => (string)$settings['bank_iban'],
|
||||
'brand_logo' => (string)$settings['brand_logo'],
|
||||
'brand_color' => (string)$settings['brand_color'],
|
||||
'watermark_enabled' => (int)$settings['watermark_enabled'],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -425,6 +427,16 @@ function saas_update_tenant_settings(PDO $pdo, int $tenantId, array $input): arr
|
||||
$pdfShowEmptyRows = !empty($input['pdf_show_empty_rows']) ? 1 : 0;
|
||||
$paymentReminderEnabled = !empty($input['payment_reminder_enabled']) ? 1 : 0;
|
||||
|
||||
// Der Hinweis auf die Kaffeeliste laesst sich abschalten - ausser im
|
||||
// kostenlosen Tarif. Die Pruefung steht hier und nicht nur im Formular,
|
||||
// damit auch ein nachgebauter POST das Kaestchen nicht leeren kann. Vor
|
||||
// der Transaktion, weil billing_fetch_or_init() eine fehlende Zeile
|
||||
// anlegt.
|
||||
$watermarkEnabled = !empty($input['watermark_enabled']) ? 1 : 0;
|
||||
if (billing_fetch_or_init($pdo, $tenantId)['plan_code'] === 'free') {
|
||||
$watermarkEnabled = 1;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
@@ -443,8 +455,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, brand_color)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
bank_account_holder, bank_iban, brand_color, watermark_enabled)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
mark_price_cents = VALUES(mark_price_cents),
|
||||
self_entry_enabled = VALUES(self_entry_enabled),
|
||||
@@ -464,7 +476,8 @@ function saas_update_tenant_settings(PDO $pdo, int $tenantId, array $input): arr
|
||||
bank_transfer_enabled = VALUES(bank_transfer_enabled),
|
||||
bank_account_holder = VALUES(bank_account_holder),
|
||||
bank_iban = VALUES(bank_iban),
|
||||
brand_color = VALUES(brand_color)'
|
||||
brand_color = VALUES(brand_color),
|
||||
watermark_enabled = VALUES(watermark_enabled)'
|
||||
);
|
||||
$stmt->execute([
|
||||
$tenantId,
|
||||
@@ -487,6 +500,7 @@ function saas_update_tenant_settings(PDO $pdo, int $tenantId, array $input): arr
|
||||
$bankAccountHolder,
|
||||
$bankIban,
|
||||
$brandColor,
|
||||
$watermarkEnabled,
|
||||
]);
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
@@ -215,3 +215,17 @@ a.plan-watermark {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Festgestellte Kaestchen (z. B. der Hinweis auf die Kaffeeliste im
|
||||
* kostenlosen Tarif)
|
||||
*
|
||||
* Das Theme zeichnet das Kaestchen ueber das folgende <label>; ein disabled
|
||||
* gesetztes Feld saehe sonst genauso aus wie ein bedienbares. Der gedimmte
|
||||
* Zustand macht sichtbar, dass hier nichts zu klicken ist - die Begruendung
|
||||
* steht als Hinweis daneben.
|
||||
* ------------------------------------------------------------------------ */
|
||||
input[type="checkbox"]:disabled + label {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
-- Schalter fuer den Hinweis auf die Kaffeeliste (Logo unten rechts).
|
||||
--
|
||||
-- Standard ist "an": die Marke ist Teil der Oberflaeche, wer sie nicht
|
||||
-- moechte, blendet sie aus. Im kostenlosen Tarif bleibt sie unabhaengig von
|
||||
-- diesem Wert eingeblendet - erzwungen in saas_update_tenant_settings() und
|
||||
-- app/plan-watermark.php, damit auch ein manipulierter Formular-POST sie
|
||||
-- dort nicht abschalten kann.
|
||||
ALTER TABLE tenant_settings
|
||||
ADD COLUMN watermark_enabled TINYINT(1) NOT NULL DEFAULT 1 AFTER brand_color;
|
||||
+18
-7
@@ -328,27 +328,38 @@ mandant-einstellungen.php (vollständige Tarif-Vergleichstabelle mit Bestell-But
|
||||
gegen einen echten Test-Mandanten durchspielen (Upgrade, Downgrade
|
||||
zwischen zwei bezahlten Stufen, Downgrade auf `free`).
|
||||
|
||||
## Wasserzeichen im kostenlosen Tarif (umgesetzt)
|
||||
## Hinweis auf die Kaffeeliste (Wasserzeichen, 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.
|
||||
Stand: 2026-08-21. Unten rechts steht eine kleine Marke mit Logo und
|
||||
Adresse der Kaffeeliste. Im Tarif `free` (bis 10 Teilnehmer) ist sie fest
|
||||
eingeschaltet; ab einem bezahlten Tarif entscheidet der Mandant selbst
|
||||
(Mandant-Einstellungen, Standard: an).
|
||||
|
||||
Umgesetzte Dateien:
|
||||
|
||||
```text
|
||||
app/plan-watermark.php (Sichtbarkeit + Markup)
|
||||
app/bootstrap.php (app_marketing_url())
|
||||
app/saas-auth.php (Speichern, Zwang im kostenlosen Tarif)
|
||||
mandant-einstellungen.php (Schalter + Hinweis)
|
||||
database/migrations/0027_watermark_setting.sql (Spalte watermark_enabled)
|
||||
footer.php (Einbindung vor den Skripten)
|
||||
assets/css/app.css (.plan-watermark)
|
||||
scripts/http-smoke.php (Dashboard-Check)
|
||||
scripts/http-smoke.php, scripts/check-m3-settings-flow.php (Regressionstests)
|
||||
```
|
||||
|
||||
- **Nur der Tarif entscheidet.** `plan_watermark_visible()` blendet die
|
||||
Marke ein, wenn `tenant_billing.plan_code` `free` ist – fehlt der
|
||||
- **Kostenlos heißt fest eingeschaltet.** `plan_watermark_visible()` zeigt
|
||||
die Marke immer, 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.
|
||||
- **Ab einem bezahlten Tarif entscheidet der Mandant.** Die Einstellung
|
||||
`tenant_settings.watermark_enabled` (Standard 1) steuert die Anzeige;
|
||||
der Schalter steht in den Mandant-Einstellungen. Im kostenlosen Tarif
|
||||
wird das Kästchen angehakt und `disabled` dargestellt, mit einem Hinweis
|
||||
daneben. Verlassen kann man sich darauf nicht – `saas_update_tenant_settings()`
|
||||
erzwingt den Wert im Tarif `free` noch einmal, damit auch ein
|
||||
nachgebauter POST ihn nicht abschalten kann.
|
||||
- **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.
|
||||
|
||||
@@ -90,6 +90,7 @@ if ($canManageSettings) {
|
||||
'payment_reminder_interval_days' => $_POST['payment_reminder_interval_days'] ?? 7,
|
||||
'brand_color' => $_POST['brand_color'] ?? '',
|
||||
'brand_logo' => (string)(saas_fetch_tenant_settings($pdo, (int)$user['tenant_id'])['brand_logo'] ?? ''),
|
||||
'watermark_enabled' => !empty($_POST['watermark_enabled']) ? 1 : 0,
|
||||
];
|
||||
}
|
||||
} else {
|
||||
@@ -256,6 +257,29 @@ include 'nav.php';
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
// Der Hinweis auf die Kaffeeliste (Logo unten rechts) ist im kostenlosen
|
||||
// Tarif Teil des Angebots und deshalb dort festgestellt. Das Kaestchen bleibt
|
||||
// sichtbar, damit erkennbar ist, was passiert - nur eben unveraenderlich.
|
||||
$watermarkErzwungen = (string)($billing['plan_code'] ?? 'free') === 'free';
|
||||
$watermarkAn = $watermarkErzwungen || (int)($settings['watermark_enabled'] ?? 1) === 1;
|
||||
?>
|
||||
<div class="col-12">
|
||||
<h3>Hinweis auf die Kaffeeliste</h3>
|
||||
<p>Unten rechts steht eine kleine Marke mit dem Logo der Kaffeeliste und
|
||||
einem Link auf unsere Seite.</p>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<input type="checkbox" id="watermark_enabled" name="watermark_enabled" value="1"
|
||||
<?php echo $watermarkAn ? 'checked' : ''; ?>
|
||||
<?php echo $watermarkErzwungen ? 'disabled' : ''; ?>>
|
||||
<label for="watermark_enabled">Logo der Kaffeeliste unten rechts anzeigen</label>
|
||||
<?php if ($watermarkErzwungen): ?>
|
||||
<div class="hint-box"><p>Im kostenlosen Tarif bleibt dieser Hinweis eingeblendet –
|
||||
er ist der Gegenwert für die kostenlose Nutzung. Ab dem Tarif „Basic" könnt ihr
|
||||
ihn hier ausschalten.</p></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="col-6 col-12-small">
|
||||
<input type="checkbox" id="payment_reminder_enabled" name="payment_reminder_enabled" value="1" <?php echo (int)$settings['payment_reminder_enabled'] === 1 ? 'checked' : ''; ?>>
|
||||
<label for="payment_reminder_enabled">Automatische Zahlungserinnerung ab Warnschwelle</label>
|
||||
|
||||
@@ -106,7 +106,41 @@ if ($registration['ok']) {
|
||||
settings_check_assert('self entry is enabled', $updated['self_entry_enabled'] === 1, $failures, $passes);
|
||||
settings_check_assert('pdf row height is updated', (int)$updated['pdf_row_height_px'] === 18, $failures, $passes);
|
||||
settings_check_assert('reminder interval is updated', (int)$updated['payment_reminder_interval_days'] === 14, $failures, $passes);
|
||||
// Das Formular oben schickt kein watermark_enabled - im kostenlosen
|
||||
// Tarif bleibt der Hinweis auf die Kaffeeliste trotzdem an.
|
||||
settings_check_assert('watermark stays on in the free plan', (int)$updated['watermark_enabled'] === 1, $failures, $passes);
|
||||
}
|
||||
|
||||
// Ab einem bezahlten Tarif entscheidet der Mandant selbst.
|
||||
$paidInput = [
|
||||
'tenant_name' => 'M3 Settings Flow Updated',
|
||||
'timezone' => 'Europe/Berlin',
|
||||
'locale' => 'de-DE',
|
||||
'currency_code' => 'EUR',
|
||||
'mark_price' => '0,35',
|
||||
'sheet_window_days' => '90',
|
||||
'negative_warning' => '12,50',
|
||||
'pdf_split_mode' => 'alphabetical',
|
||||
'pdf_row_height_px' => '18',
|
||||
'payment_reminder_interval_days' => '14',
|
||||
];
|
||||
billing_update($pdo, $tenantId, ['plan_code' => 'basic']);
|
||||
|
||||
$paidOff = saas_update_tenant_settings($pdo, $tenantId, $paidInput);
|
||||
settings_check_assert(
|
||||
'watermark can be switched off on a paid plan',
|
||||
$paidOff['ok'] === true && (int)$paidOff['settings']['watermark_enabled'] === 0,
|
||||
$failures,
|
||||
$passes
|
||||
);
|
||||
|
||||
$paidOn = saas_update_tenant_settings($pdo, $tenantId, $paidInput + ['watermark_enabled' => '1']);
|
||||
settings_check_assert(
|
||||
'watermark can be switched back on',
|
||||
$paidOn['ok'] === true && (int)$paidOn['settings']['watermark_enabled'] === 1,
|
||||
$failures,
|
||||
$passes
|
||||
);
|
||||
}
|
||||
|
||||
settings_check_cleanup($pdo, $slug, $emailNorm);
|
||||
|
||||
+18
-1
@@ -5,7 +5,11 @@ declare(strict_types=1);
|
||||
require __DIR__ . '/dev-db.php';
|
||||
|
||||
$baseUrl = rtrim((string)(getenv('SMOKE_BASE_URL') ?: 'http://127.0.0.1:8080'), '/');
|
||||
$issuePattern = '~(?:Deprecated|Warning|Fatal error|Parse error|Notice|Uncaught (?:Error|Exception))~i';
|
||||
// PHP-Meldungen enden immer auf einen Doppelpunkt ("Warning: ...", in HTML
|
||||
// auch "<b>Warning</b>: ..."). Der verlangte Doppelpunkt haelt harmlose
|
||||
// Treffer heraus, die dieselben Woerter enthalten - z. B. das Feld
|
||||
// "negative_warning" in den Mandant-Einstellungen.
|
||||
$issuePattern = '~(?:Deprecated|Warning|Fatal error|Parse error|Notice|Uncaught (?:Error|Exception))(?:</b>)?\s*:~i';
|
||||
|
||||
// Der Smoke-Test legt sich seinen eigenen Mandanten mit bekannten Zahlen an und
|
||||
// meldet sich per HTTP an, statt sich auf vorhandene Daten oder eine
|
||||
@@ -171,6 +175,19 @@ $checks = [
|
||||
'expect_redirect' => 'login.php',
|
||||
'contains' => [],
|
||||
],
|
||||
[
|
||||
// Der Smoke-Mandant ist im kostenlosen Tarif: der Schalter fuer den
|
||||
// Hinweis auf die Kaffeeliste muss erscheinen, aber festgestellt sein.
|
||||
'label' => 'Mandant-Einstellungen',
|
||||
'path' => 'mandant-einstellungen.php',
|
||||
'auth' => true,
|
||||
'contains' => [
|
||||
'Hinweis auf die Kaffeeliste',
|
||||
'Logo der Kaffeeliste unten rechts anzeigen',
|
||||
'Im kostenlosen Tarif bleibt dieser Hinweis eingeblendet',
|
||||
'disabled',
|
||||
],
|
||||
],
|
||||
[
|
||||
'label' => 'Namensanpassung',
|
||||
'path' => 'namenanpassen.php',
|
||||
|
||||
Reference in New Issue
Block a user