From c9ab82e97a4070e1d4889d87dbb84b297f50e765 Mon Sep 17 00:00:00 2001 From: Clemens Creutzburg Date: Mon, 20 Jul 2026 00:50:29 +0200 Subject: [PATCH] PDF Erstellung anpassung --- exportKaffeeliste.php | 246 ++++++++++++++++++++++++++---------------- 1 file changed, 151 insertions(+), 95 deletions(-) diff --git a/exportKaffeeliste.php b/exportKaffeeliste.php index 51c1db7..330d589 100644 --- a/exportKaffeeliste.php +++ b/exportKaffeeliste.php @@ -38,99 +38,80 @@ $footerText = trim((string)($settings['pdf_footer_text'] ?? '')); // Ab dieser Mitgliederzahl wird auf zwei Seiten (Vorder-/Rueckseite) statt // einer einzelnen Seite gedruckt. define('EXPORT_MULTI_PAGE_THRESHOLD', 50); -// Platzbudget in TCPDF-HTML-"px" fuer die Datenzeilen einer Seite, kalibriert -// an der bisherigen Ausgabe (63 Zeilen a 16px auf einer vollen A4-Seite). Ist -// eine Fusszeile konfiguriert, wird ihr reservierter Platz abgezogen, damit -// die letzte Tabellenzeile sie nicht ueberlappt. -define('EXPORT_FOOTER_RESERVE_PX', 40); -define('EXPORT_PAGE_ROW_BUDGET_PX', (63 * 16) - ($footerText !== '' ? EXPORT_FOOTER_RESERVE_PX : 0)); -// Technischer Mindestwert, nur als letzte Bremse gegen 0/negative Hoehen bei -// absurd hohen Teilnehmerzahlen - kein gestalterisches Minimum. -define('EXPORT_HARD_MIN_ROW_HEIGHT_PX', 6); +// TCPDF's writeHTML() ignoriert CSS-Zeilenhoehen/-innenabstaende auf +// Tabellenzeilen vollstaendig (getestet: weder "height" auf noch +// "padding" auf haben irgendeinen Einfluss auf die gerenderte Zeilen- +// hoehe). Deshalb wird die Tabelle unten mit TCPDFs nativer Cell()-Methode +// gezeichnet, die den Hoehen-Parameter zuverlaessig exakt umsetzt. +define('EXPORT_PAGE_WIDTH_MM', 200.0); // A4 (210mm) minus 5mm Rand links/rechts +define('EXPORT_COL_NAME_MM', 40.0); +define('EXPORT_COL_BALANCE_MM', 20.0); +define('EXPORT_COL_MARKS_MM', EXPORT_PAGE_WIDTH_MM - EXPORT_COL_NAME_MM - EXPORT_COL_BALANCE_MM); +define('EXPORT_TITLE_ROW_MM', 8.0); +define('EXPORT_HEADER_ROW_MM', 6.0); +define('EXPORT_HINT_ROW_MM', 6.0); +// 1px (CSS/96dpi) = 25.4/96 mm - fuer die Umrechnung der Admin-Einstellung +// (weiterhin in "px" gepflegt, siehe mandant-einstellungen.php) in mm. +define('EXPORT_PX_TO_MM', 25.4 / 96); +define('EXPORT_HARD_MIN_ROW_HEIGHT_MM', 2.0); +// TCPDFs Cell() verbraucht pro gezeichneter Zeile durchgaengig etwas mehr +// vertikalen Platz als die angeforderte Hoehe (mutmasslich die +// Rahmenlinienbreite), gemessen ueber 61 Testzeilen a 4.2333mm: ca. 0,157mm +// Overhead je Zeile. Ohne Beruecksichtigung summiert sich das ueber viele +// Zeilen zu mehreren mm und kann einen ungewollten Seitenumbruch ausloesen. +define('EXPORT_ROW_OVERHEAD_MM', 0.2); $allActive = ledger_fetch_participant_summaries($pdo, $tenantId, ['active_only' => true]); $memberCount = count($allActive); $isMultiPage = $memberCount >= EXPORT_MULTI_PAGE_THRESHOLD; /** - * Ermittelt Zeilenanzahl und -hoehe fuer eine Seite: Ausgangspunkt ist die - * vom Admin gewuenschte Zeilenhoehe. Sind Fuellzeilen erwuenscht, wird die - * Seite bis zu dieser Hoehe komplett aufgefuellt (weniger Mitglieder -> + * Verfuegbare Hoehe (mm) fuer die Datenzeilen einer A4-Seite: Gesamthoehe + * 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 +{ + // Kleiner zusaetzlicher Sicherheitspuffer obendrauf (der zeilenweise + // Cell()-Overhead wird bereits separat in export_compute_layout() + // eingerechnet). + $budget = 297.0 - 5.0 - 5.0 - EXPORT_TITLE_ROW_MM - EXPORT_HEADER_ROW_MM - 2.0; + if ($hasFooter) { + $budget -= 12.0; + } + if ($hasSeitenHinweis) { + $budget -= EXPORT_HINT_ROW_MM; + } + + return max(EXPORT_HARD_MIN_ROW_HEIGHT_MM, $budget); +} + +/** + * Ermittelt Zeilenanzahl und -hoehe (mm) fuer eine Seite: Ausgangspunkt ist + * die vom Admin gewuenschte Zeilenhoehe. Sind Fuellzeilen erwuenscht, wird + * die Seite bis zu dieser Hoehe komplett aufgefuellt (weniger Mitglieder -> * groessere, statt immer gleich viele Zeilen). Passt die Mitgliederzahl bei * der gewuenschten Hoehe nicht auf eine Seite, wird bei jeder Generierung * automatisch so weit verkleinert, dass garantiert alle Teilnehmer passen. * - * @return array{0: int, 1: float} [targetRows, rowHeightPx] + * @return array{0: int, 1: float} [targetRows, rowHeightMm] */ -function export_compute_layout(int $memberCount, bool $showEmptyRows, int $adminRowHeightPx): array +function export_compute_layout(int $memberCount, bool $showEmptyRows, int $adminRowHeightPx, float $pageRowBudgetMm): array { - $adminRowHeightPx = max(1, $adminRowHeightPx); - $capacityAtAdminHeight = (int)floor(EXPORT_PAGE_ROW_BUDGET_PX / $adminRowHeightPx); + $adminRowHeightMm = max(0.1, $adminRowHeightPx * EXPORT_PX_TO_MM); + // Jede Zeile verbraucht real "Hoehe + Overhead" (siehe EXPORT_ROW_OVERHEAD_MM), + // das muss bei der Kapazitaets- und Hoehenberechnung mit einfliessen. + $capacityAtAdminHeight = (int)floor($pageRowBudgetMm / ($adminRowHeightMm + EXPORT_ROW_OVERHEAD_MM)); $targetRows = $showEmptyRows ? max($memberCount, $capacityAtAdminHeight) : $memberCount; $targetRows = max(1, $targetRows); - $rowHeight = min((float)$adminRowHeightPx, EXPORT_PAGE_ROW_BUDGET_PX / $targetRows); - $rowHeight = max(EXPORT_HARD_MIN_ROW_HEIGHT_PX, $rowHeight); + $rowHeight = min($adminRowHeightMm, ($pageRowBudgetMm / $targetRows) - EXPORT_ROW_OVERHEAD_MM); + $rowHeight = max(EXPORT_HARD_MIN_ROW_HEIGHT_MM, $rowHeight); return [$targetRows, $rowHeight]; } -/** - * Rendert eine Teilnehmerseite als HTML-Tabelle fuer TCPDF. - * - * @param list $teilnehmer - */ -function export_render_page_html(array $teilnehmer, string $titel, string $hinweisText, string $markPrice, bool $showEmptyRows, int $adminRowHeightPx, ?string $seitenHinweis): string -{ - [$targetRows, $rowHeight] = export_compute_layout(count($teilnehmer), $showEmptyRows, $adminRowHeightPx); - - $html = ' - - - - - - - - - - '; - - $rowCount = 0; - foreach ($teilnehmer as $person) { - $rowCount++; - $name = saas_html($person['display_name']); - $balanceCents = (int)$person['balance_cents']; - $balance = number_format($balanceCents / 100, 2, ',', '.'); - - $html .= ''; - $html .= ""; - if ($balanceCents < -1000) { - $html .= ''; - } elseif ($balanceCents > 500) { - $html .= ''; - } else { - $html .= ""; - } - $html .= ""; - $html .= ""; - } - - if ($showEmptyRows) { - for ($i = $rowCount; $i < $targetRows; $i++) { - $html .= ''; - } - } - - if ($seitenHinweis !== null) { - $html .= ''; - } - $html .= '
' . $titel . '1 Strich = ' . $markPrice . '€. Bitte bezahlen bei 10 € zahlen. ' . date("d.m.Y H:s") . '
NameGuthabenStriche
{$name}' . $balance . ' €' . $balance . ' €{$balance} €
   
  ' . $seitenHinweis . '
'; - - return $html; -} - // TCPDF-Bibliothek einbinden require_once(__DIR__ . '/TCPDF/tcpdf.php'); @@ -146,15 +127,25 @@ class MyCustomPDFWithWatermark extends TCPDF { $bMargin = $this->getBreakMargin(); $auto_page_break = $this->AutoPageBreak; $this->SetAutoPageBreak(false, 0); + $x = $this->x; + $y = $this->y; $this->SetAlpha(0.35); $this->SetFont('helvetica', '', 14); $this->SetTextColor(0, 0, 0); - foreach ([40, 140, 240] as $y) { - $this->MultiCell(90, 0, $this->watermarkText, 0, 'L', false, 1, 110, $y); + foreach ([40, 140, 240] as $wy) { + $this->MultiCell(90, 0, $this->watermarkText, 0, 'L', false, 1, 110, $wy); } $this->SetAlpha(1); $this->SetFont('helvetica', '', 9.5); + $this->SetTextColor(0, 0, 0); + + // MultiCell(..., $ln=1, ...) verschiebt den Seiten-Cursor als + // Nebeneffekt - ohne dieses Zuruecksetzen wuerde der eigentliche + // Tabelleninhalt an der Position des letzten Wasserzeichen-Absatzes + // statt oben auf der Seite beginnen. + $this->x = $x; + $this->y = $y; $this->SetAutoPageBreak($auto_page_break, $bMargin); $this->setPageMark(); @@ -173,6 +164,88 @@ class MyCustomPDFWithWatermark extends TCPDF { } } +/** + * Zeichnet eine Teilnehmerseite mit TCPDFs nativer Cell()-Methode (siehe + * Kommentar oben zu EXPORT_PAGE_WIDTH_MM: writeHTML() ignoriert Zeilenhoehen + * auf / komplett, Cell() setzt die uebergebene Hoehe dagegen exakt + * um). + * + * @param list $teilnehmer + * @param array{0:int,1:int,2:int} $titelFarbe Hintergrundfarbe der Titelzeile + * @param array{0:int,1:int,2:int} $titelTextFarbe Textfarbe der Titelzeile + */ +function export_render_page( + TCPDF $pdf, + array $teilnehmer, + string $titel, + array $titelFarbe, + array $titelTextFarbe, + string $markPrice, + bool $showEmptyRows, + int $adminRowHeightPx, + ?string $seitenHinweis +): void { + $rowBudgetMm = export_page_row_budget_mm(trim($pdf->footerText) !== '', $seitenHinweis !== null); + [$targetRows, $rowHeight] = export_compute_layout(count($teilnehmer), $showEmptyRows, $adminRowHeightPx, $rowBudgetMm); + + // Titelzeile: Name/Guthaben-Breite + Strichebreite, Hintergrundfarbe je nach Seite. + $pdf->SetFont('helvetica', 'B', 13); + $pdf->SetFillColor($titelFarbe[0], $titelFarbe[1], $titelFarbe[2]); + $pdf->SetTextColor($titelTextFarbe[0], $titelTextFarbe[1], $titelTextFarbe[2]); + $pdf->Cell(EXPORT_COL_NAME_MM + EXPORT_COL_BALANCE_MM, EXPORT_TITLE_ROW_MM, $titel, 1, 0, 'L', true); + $pdf->SetFont('helvetica', '', 9); + $hinweisText = '1 Strich = ' . $markPrice . '€. Bitte bezahlen bei 10 € zahlen. ' . date('d.m.Y H:i'); + $pdf->Cell(EXPORT_COL_MARKS_MM, EXPORT_TITLE_ROW_MM, $hinweisText, 1, 1, 'L', true); + $pdf->SetTextColor(0, 0, 0); + + // Spaltenkopf. + $pdf->SetFont('helvetica', 'B', 9.5); + $pdf->Cell(EXPORT_COL_NAME_MM, EXPORT_HEADER_ROW_MM, 'Name', 1, 0, 'L'); + $pdf->Cell(EXPORT_COL_BALANCE_MM, EXPORT_HEADER_ROW_MM, 'Guthaben', 1, 0, 'L'); + $pdf->Cell(EXPORT_COL_MARKS_MM, EXPORT_HEADER_ROW_MM, 'Striche', 1, 1, 'L'); + + // Datenzeilen. + $pdf->SetFont('helvetica', '', 9.5); + $rowCount = 0; + foreach ($teilnehmer as $person) { + $rowCount++; + $name = (string)$person['display_name']; + $balanceCents = (int)$person['balance_cents']; + $balance = number_format($balanceCents / 100, 2, ',', '.') . ' €'; + + $pdf->Cell(EXPORT_COL_NAME_MM, $rowHeight, $name, 1, 0, 'L'); + if ($balanceCents < -1000) { + $pdf->SetFillColor(204, 0, 0); + $pdf->SetTextColor(255, 255, 255); + $pdf->Cell(EXPORT_COL_BALANCE_MM, $rowHeight, $balance, 1, 0, 'L', true); + $pdf->SetTextColor(0, 0, 0); + } elseif ($balanceCents > 500) { + $pdf->SetFillColor(0, 102, 0); + $pdf->SetTextColor(255, 255, 255); + $pdf->Cell(EXPORT_COL_BALANCE_MM, $rowHeight, $balance, 1, 0, 'L', true); + $pdf->SetTextColor(0, 0, 0); + } else { + $pdf->Cell(EXPORT_COL_BALANCE_MM, $rowHeight, $balance, 1, 0, 'L'); + } + $pdf->Cell(EXPORT_COL_MARKS_MM, $rowHeight, '', 1, 1, 'L'); + } + + if ($showEmptyRows) { + for ($i = $rowCount; $i < $targetRows; $i++) { + $pdf->Cell(EXPORT_COL_NAME_MM, $rowHeight, '', 1, 0, 'L'); + $pdf->Cell(EXPORT_COL_BALANCE_MM, $rowHeight, '', 1, 0, 'L'); + $pdf->Cell(EXPORT_COL_MARKS_MM, $rowHeight, '', 1, 1, 'L'); + } + } + + if ($seitenHinweis !== null) { + $pdf->Cell(EXPORT_COL_NAME_MM + EXPORT_COL_BALANCE_MM, EXPORT_HINT_ROW_MM, '', 1, 0, 'L'); + $pdf->SetFont('helvetica', 'B', 9.5); + $pdf->SetFillColor(255, 255, 0); + $pdf->Cell(EXPORT_COL_MARKS_MM, EXPORT_HINT_ROW_MM, $seitenHinweis, 1, 1, 'R', true); + } +} + $pdf = new MyCustomPDFWithWatermark(PDF_PAGE_ORIENTATION, 'mm', 'A4', true, 'UTF-8', false); $pdf->watermarkText = $watermarkText; $pdf->footerText = $footerText; @@ -184,18 +257,7 @@ $pdf->SetFont('helvetica', '', 9.5); if (!$isMultiPage) { $pdf->AddPage(); - $pdf->writeHTML( - export_render_page_html( - $allActive, - 'Kaffeeliste', - 'background-color: rgb(0, 94, 63);color: rgb(255, 255, 255);', - $markPrice, - $showEmptyRows, - $adminRowHeightPx, - null - ), - true, false, true, false, '' - ); + export_render_page($pdf, $allActive, 'Kaffeeliste', [0, 94, 63], [255, 255, 255], $markPrice, $showEmptyRows, $adminRowHeightPx, null); } else { if ($splitMode === 'alphabetical') { $mid = (int)ceil($memberCount / 2); @@ -211,16 +273,10 @@ if (!$isMultiPage) { } $pdf->AddPage(); - $pdf->writeHTML( - export_render_page_html($front, $frontTitel, 'background-color: rgb(0, 94, 63);color: rgb(255, 255, 255);', $markPrice, $showEmptyRows, $adminRowHeightPx, 'Rückseite beachten!'), - true, false, true, false, '' - ); + export_render_page($pdf, $front, $frontTitel, [0, 94, 63], [255, 255, 255], $markPrice, $showEmptyRows, $adminRowHeightPx, 'Rückseite beachten!'); $pdf->AddPage(); - $pdf->writeHTML( - export_render_page_html($back, $backTitel, 'background-color: rgb(91, 209, 215);color: rgb(0, 0, 0);', $markPrice, $showEmptyRows, $adminRowHeightPx, 'Vorderseite beachten!'), - true, false, true, false, '' - ); + export_render_page($pdf, $back, $backTitel, [91, 209, 215], [0, 0, 0], $markPrice, $showEmptyRows, $adminRowHeightPx, 'Vorderseite beachten!'); } $pdf->Output('Kaffeestrichliste.pdf', 'D');