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>'; . '</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, * Markup des Wasserzeichens. Ohne erreichbare Marketing-Domain (CLI,
* ungewoehnliche Hosts) bleibt es bei Logo und Namen ohne Link - ein * 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 function plan_watermark_html(): string
{ {
$url = app_marketing_url(); $url = app_marketing_url();
$label = $url !== '' ? preg_replace('#^https?://#i', '', $url) : 'Kaffeeliste'; $label = plan_watermark_label();
$inner = plan_watermark_logo_svg() $inner = plan_watermark_logo_svg()
. '<span class="plan-watermark-text">' . htmlspecialchars($label, ENT_QUOTES, 'UTF-8') . '</span>'; . '<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: Umgesetzte Dateien:
```text ```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/bootstrap.php (app_marketing_url())
app/saas-auth.php (Speichern, Zwang im kostenlosen Tarif) app/saas-auth.php (Speichern, Zwang im kostenlosen Tarif)
mandant-einstellungen.php (Schalter + Hinweis) 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 - **Keine Schreibzugriffe im Seitenaufbau.** Bewusst nicht
`billing_fetch_or_init()`, das eine fehlende Zeile anlegen würde; die `billing_fetch_or_init()`, das eine fehlende Zeile anlegen würde; die
Prüfung ist eine reine Leseabfrage und wird pro Request zwischengespeichert. 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 - **Logo als Inline-SVG.** Spart einen Request, bleibt auf jedem Bildschirm
scharf und übernimmt die Farbe der Umgebung. Ein eigenes Bild lässt sich scharf und übernimmt die Farbe der Umgebung. Ein eigenes Bild lässt sich
in `plan_watermark_logo_svg()` durch ein `<img>` ersetzen. 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'] ?? '')); $footerText = trim((string)($settings['pdf_footer_text'] ?? ''));
$watermarkLogoPath = tenant_logo_path((string)($settings['pdf_watermark_logo'] ?? '')) ?? ''; $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 // Vorder-/Rueckseite erst oberhalb der gemeinsamen Schwelle aus app/ledger.php
// (LEDGER_SINGLE_PAGE_MAX_MEMBERS): bis einschliesslich 50 Personen bleibt der // (LEDGER_SINGLE_PAGE_MAX_MEMBERS): bis einschliesslich 50 Personen bleibt der
// Ausdruck bewusst einseitig. // Ausdruck bewusst einseitig.
@@ -70,7 +75,7 @@ $isMultiPage = ledger_uses_two_sided_sheet($memberCount);
* minus oberem/unterem Rand, Titelzeile, Spaltenkopf und optional * minus oberem/unterem Rand, Titelzeile, Spaltenkopf und optional
* reservierter Fusszeile bzw. Vorder-/Rueckseiten-Hinweiszeile. * 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 // Kleiner zusaetzlicher Sicherheitspuffer obendrauf (der zeilenweise
// Cell()-Overhead wird bereits separat in export_compute_layout() // 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; $budget = 297.0 - 5.0 - 5.0 - EXPORT_TITLE_ROW_MM - EXPORT_HEADER_ROW_MM - 2.0;
if ($hasFooter) { if ($hasFooter) {
$budget -= 12.0; $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) { if ($hasSeitenHinweis) {
$budget -= EXPORT_HINT_ROW_MM; $budget -= EXPORT_HINT_ROW_MM;
@@ -119,6 +128,7 @@ class MyCustomPDFWithWatermark extends TCPDF {
public string $watermarkText = ''; public string $watermarkText = '';
public string $watermarkLogoPath = ''; public string $watermarkLogoPath = '';
public string $footerText = ''; public string $footerText = '';
public string $planWatermarkLabel = '';
public function Header() { public function Header() {
$hasLogo = $this->watermarkLogoPath !== '' && is_file($this->watermarkLogoPath); $hasLogo = $this->watermarkLogoPath !== '' && is_file($this->watermarkLogoPath);
@@ -163,16 +173,70 @@ class MyCustomPDFWithWatermark extends TCPDF {
} }
public function Footer() { public function Footer() {
if (trim($this->footerText) === '') { if (trim($this->footerText) !== '') {
return;
}
$this->SetY(-15); $this->SetY(-15);
$this->SetFont('helvetica', '', 8); $this->SetFont('helvetica', '', 8);
$this->SetTextColor(120, 120, 120); $this->SetTextColor(120, 120, 120);
$this->Cell(0, 10, $this->footerText, 0, 0, 'C'); $this->Cell(0, 10, $this->footerText, 0, 0, 'C');
$this->SetTextColor(0, 0, 0); $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, int $adminRowHeightPx,
?string $seitenHinweis ?string $seitenHinweis
): void { ): 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); [$targetRows, $rowHeight] = export_compute_layout(count($teilnehmer), $showEmptyRows, $adminRowHeightPx, $rowBudgetMm);
// Titelzeile: Name/Guthaben-Breite + Strichebreite, Hintergrundfarbe je nach Seite. // 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->watermarkText = $watermarkText;
$pdf->watermarkLogoPath = $watermarkLogoPath; $pdf->watermarkLogoPath = $watermarkLogoPath;
$pdf->footerText = $footerText; $pdf->footerText = $footerText;
$pdf->planWatermarkLabel = $planWatermarkLabel;
$pdf->SetHeaderData("", 0, "Kaffeestrichliste", ""); $pdf->SetHeaderData("", 0, "Kaffeestrichliste", "");
$pdf->SetPrintFooter(true); $pdf->SetPrintFooter(true);
$pdf->SetMargins('5', '5', '5'); $pdf->SetMargins('5', '5', '5');