From 9b309bb942809e7a1d638c7dca1cf97ad88d3928 Mon Sep 17 00:00:00 2001 From: Clemens Creutzburg Date: Fri, 21 Aug 2026 00:46:52 +0200 Subject: [PATCH] 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 --- app/plan-watermark.php | 15 +++++++- docs/billing.md | 11 +++++- exportKaffeeliste.php | 83 ++++++++++++++++++++++++++++++++++++++---- 3 files changed, 100 insertions(+), 9 deletions(-) diff --git a/app/plan-watermark.php b/app/plan-watermark.php index ae1119e..6d42ab0 100644 --- a/app/plan-watermark.php +++ b/app/plan-watermark.php @@ -85,6 +85,19 @@ function plan_watermark_logo_svg(): string . ''; } +/** + * 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() . '' . htmlspecialchars($label, ENT_QUOTES, 'UTF-8') . ''; diff --git a/docs/billing.md b/docs/billing.md index 78464da..bbe782b 100644 --- a/docs/billing.md +++ b/docs/billing.md @@ -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 `` ersetzen. diff --git a/exportKaffeeliste.php b/exportKaffeeliste.php index e79aaba..5ba96f7 100644 --- a/exportKaffeeliste.php +++ b/exportKaffeeliste.php @@ -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,15 +173,69 @@ class MyCustomPDFWithWatermark extends TCPDF { } public function Footer() { - if (trim($this->footerText) === '') { + 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; } - $this->SetY(-15); - $this->SetFont('helvetica', '', 8); - $this->SetTextColor(120, 120, 120); - $this->Cell(0, 10, $this->footerText, 0, 0, 'C'); + $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');