Hinweis auf die Kaffeeliste auch im PDF-Ausdruck

Die Marke stand bisher nur in der Web-Ansicht; auf dem Ausdruck, den die
Mitglieder taeglich vor sich haben, fehlte sie. Sie erscheint jetzt unten
rechts im Seitenfuss - nach derselben Regel wie im Web: im kostenlosen
Tarif fest, sonst nach Mandant-Einstellung.

Die Tasse wird mit TCPDFs Zeichenbefehlen gemalt statt als SVG oder Bild
eingebettet: ImageSVG braucht die XML-Extension, ein Rasterbild GD - beides
ist auf einem Webspace nicht garantiert.

Der Schriftzug sitzt in einer Cell() statt in Text(): dessen y-Bezug haengt
an Schriftmetriken und ergab in der Probe einen um Millimeter versetzten
Schriftzug. Steht die Marke, reserviert export_page_row_budget_mm() 6 mm am
Seitenfuss, damit die Tabelle nicht hineinlaeuft.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-21 00:46:52 +02:00
co-authored by Claude Opus 5
parent 7a52099a59
commit 9b309bb942
3 changed files with 100 additions and 9 deletions
+14 -1
View File
@@ -85,6 +85,19 @@ function plan_watermark_logo_svg(): string
. '</svg>';
}
/**
* Beschriftung des Wasserzeichens: die Marketing-Domain ohne Schema
* ("kaffeeliste.de"). Laesst sie sich nicht ermitteln, bleibt der Name der
* App uebrig. Wird von der Web-Ansicht und vom PDF-Ausdruck gemeinsam
* genutzt, damit beide dieselbe Adresse nennen.
*/
function plan_watermark_label(): string
{
$url = app_marketing_url();
return $url !== '' ? (string)preg_replace('#^https?://#i', '', $url) : 'Kaffeeliste';
}
/**
* Markup des Wasserzeichens. Ohne erreichbare Marketing-Domain (CLI,
* ungewoehnliche Hosts) bleibt es bei Logo und Namen ohne Link - ein
@@ -93,7 +106,7 @@ function plan_watermark_logo_svg(): string
function plan_watermark_html(): string
{
$url = app_marketing_url();
$label = $url !== '' ? preg_replace('#^https?://#i', '', $url) : 'Kaffeeliste';
$label = plan_watermark_label();
$inner = plan_watermark_logo_svg()
. '<span class="plan-watermark-text">' . htmlspecialchars($label, ENT_QUOTES, 'UTF-8') . '</span>';
+10 -1
View File
@@ -338,7 +338,8 @@ eingeschaltet; ab einem bezahlten Tarif entscheidet der Mandant selbst
Umgesetzte Dateien:
```text
app/plan-watermark.php (Sichtbarkeit + Markup)
app/plan-watermark.php (Sichtbarkeit, Markup, Beschriftung)
exportKaffeeliste.php (Marke im PDF-Fuss, gezeichnet statt eingebettet)
app/bootstrap.php (app_marketing_url())
app/saas-auth.php (Speichern, Zwang im kostenlosen Tarif)
mandant-einstellungen.php (Schalter + Hinweis)
@@ -363,6 +364,14 @@ scripts/http-smoke.php, scripts/check-m3-settings-flow.php (Regressionstests)
- **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.
- **Im PDF mitgezeichnet.** Der Ausdruck traegt dieselbe Marke unten
rechts (`MyCustomPDFWithWatermark::planWatermark()`), nach derselben
Regel wie die Web-Ansicht. Die Tasse wird dort mit TCPDFs Zeichen-
befehlen gemalt: `ImageSVG()` braucht die XML-Extension, ein Rasterbild
GD - beides ist auf einem Webspace nicht garantiert. Steht die Marke,
reserviert `export_page_row_budget_mm()` 6 mm am Seitenfuss, damit die
Tabelle nicht hineinlaeuft (bei gesetzter Fusszeile deckt deren Band sie
bereits ab).
- **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.
+75 -6
View File
@@ -35,6 +35,11 @@ $watermarkText = trim((string)($settings['pdf_watermark_text'] ?? ''));
$footerText = trim((string)($settings['pdf_footer_text'] ?? ''));
$watermarkLogoPath = tenant_logo_path((string)($settings['pdf_watermark_logo'] ?? '')) ?? '';
// Hinweis auf die Kaffeeliste: im Ausdruck nach derselben Regel wie in der
// Web-Ansicht - im kostenlosen Tarif fest, sonst nach Mandant-Einstellung.
require_once __DIR__ . '/app/plan-watermark.php';
$planWatermarkLabel = plan_watermark_visible($pdo, $tenantId) ? plan_watermark_label() : '';
// 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.
@@ -70,7 +75,7 @@ $isMultiPage = ledger_uses_two_sided_sheet($memberCount);
* minus oberem/unterem Rand, Titelzeile, Spaltenkopf und optional
* reservierter Fusszeile bzw. Vorder-/Rueckseiten-Hinweiszeile.
*/
function export_page_row_budget_mm(bool $hasFooter, bool $hasSeitenHinweis): float
function export_page_row_budget_mm(bool $hasFooter, bool $hasSeitenHinweis, bool $hasPlanWatermark = false): float
{
// Kleiner zusaetzlicher Sicherheitspuffer obendrauf (der zeilenweise
// Cell()-Overhead wird bereits separat in export_compute_layout()
@@ -78,6 +83,10 @@ function export_page_row_budget_mm(bool $hasFooter, bool $hasSeitenHinweis): flo
$budget = 297.0 - 5.0 - 5.0 - EXPORT_TITLE_ROW_MM - EXPORT_HEADER_ROW_MM - 2.0;
if ($hasFooter) {
$budget -= 12.0;
} elseif ($hasPlanWatermark) {
// Der Hinweis auf die Kaffeeliste braucht deutlich weniger Platz als
// eine Fusszeile - steht beides, deckt deren Band ihn mit ab.
$budget -= 6.0;
}
if ($hasSeitenHinweis) {
$budget -= EXPORT_HINT_ROW_MM;
@@ -119,6 +128,7 @@ class MyCustomPDFWithWatermark extends TCPDF {
public string $watermarkText = '';
public string $watermarkLogoPath = '';
public string $footerText = '';
public string $planWatermarkLabel = '';
public function Header() {
$hasLogo = $this->watermarkLogoPath !== '' && is_file($this->watermarkLogoPath);
@@ -163,16 +173,70 @@ class MyCustomPDFWithWatermark extends TCPDF {
}
public function Footer() {
if (trim($this->footerText) === '') {
return;
}
if (trim($this->footerText) !== '') {
$this->SetY(-15);
$this->SetFont('helvetica', '', 8);
$this->SetTextColor(120, 120, 120);
$this->Cell(0, 10, $this->footerText, 0, 0, 'C');
$this->SetTextColor(0, 0, 0);
}
$this->planWatermark();
}
/**
* Hinweis auf die Kaffeeliste unten rechts - dasselbe Zeichen wie in der
* Web-Ansicht, nur fuer den Ausdruck. Das Tassen-Symbol wird mit TCPDFs
* eigenen Zeichenbefehlen gemalt statt als SVG oder Bild eingebettet:
* ImageSVG braucht die XML-Extension, ein Rasterbild braucht GD - beides
* ist auf einem Webspace nicht garantiert, gezeichnete Linien dagegen
* funktionieren immer.
*/
protected function planWatermark(): void
{
if (trim($this->planWatermarkLabel) === '') {
return;
}
$rechterRand = $this->getPageWidth() - 5.0;
// Text und Symbol teilen sich eine gedachte Zeile: Cell() zentriert
// den Schriftzug zuverlaessig in deren Hoehe, die Tasse wird auf
// dieselbe Mittellinie gezeichnet. Text() waere hier die schlechtere
// Wahl - sein y-Bezugspunkt haengt an Schriftmetriken und ergab in
// der Probe einen um Millimeter versetzten Schriftzug.
$zeilenHoehe = 4.0;
$zeileOben = $this->getPageHeight() - 5.0 - $zeilenHoehe;
$mitte = $zeileOben + ($zeilenHoehe / 2);
$this->SetFont('helvetica', '', 7);
$this->SetTextColor(130, 130, 130);
$textBreite = $this->GetStringWidth($this->planWatermarkLabel);
$tasseBreite = 2.6;
$tasseHoehe = 2.2;
$abstand = 1.0;
$henkelBreite = 1.0;
$tasseX = $rechterRand - $textBreite - $abstand - $tasseBreite - $henkelBreite;
$tasseY = $mitte - ($tasseHoehe / 2);
$this->SetDrawColor(130, 130, 130);
$this->SetLineWidth(0.2);
// Tassenkoerper: nur unten abgerundet ('0011' = unten rechts/links).
$this->RoundedRect($tasseX, $tasseY, $tasseBreite, $tasseHoehe, 0.7, '0011', 'D');
// Henkel: halber Bogen an der rechten Seite.
$this->Ellipse($tasseX + $tasseBreite, $mitte, 0.8, 0.6, 0, 270, 90, 'D');
// Zwei Dampffaehnchen ueber der Tasse.
$this->Line($tasseX + 0.7, $tasseY - 1.1, $tasseX + 0.7, $tasseY - 0.4);
$this->Line($tasseX + 1.7, $tasseY - 1.1, $tasseX + 1.7, $tasseY - 0.4);
$this->SetXY($rechterRand - $textBreite, $zeileOben);
$this->Cell($textBreite, $zeilenHoehe, $this->planWatermarkLabel, 0, 0, 'R');
$this->SetTextColor(0, 0, 0);
$this->SetDrawColor(0, 0, 0);
$this->SetLineWidth(0.2);
}
}
/**
@@ -196,7 +260,11 @@ function export_render_page(
int $adminRowHeightPx,
?string $seitenHinweis
): void {
$rowBudgetMm = export_page_row_budget_mm(trim($pdf->footerText) !== '', $seitenHinweis !== null);
$rowBudgetMm = export_page_row_budget_mm(
trim($pdf->footerText) !== '',
$seitenHinweis !== null,
trim($pdf->planWatermarkLabel) !== ''
);
[$targetRows, $rowHeight] = export_compute_layout(count($teilnehmer), $showEmptyRows, $adminRowHeightPx, $rowBudgetMm);
// Titelzeile: Name/Guthaben-Breite + Strichebreite, Hintergrundfarbe je nach Seite.
@@ -261,6 +329,7 @@ $pdf = new MyCustomPDFWithWatermark(PDF_PAGE_ORIENTATION, 'mm', 'A4', true, 'UTF
$pdf->watermarkText = $watermarkText;
$pdf->watermarkLogoPath = $watermarkLogoPath;
$pdf->footerText = $footerText;
$pdf->planWatermarkLabel = $planWatermarkLabel;
$pdf->SetHeaderData("", 0, "Kaffeestrichliste", "");
$pdf->SetPrintFooter(true);
$pdf->SetMargins('5', '5', '5');