From 5e6262b6a81ab594998dd52a8eeda01f39101882 Mon Sep 17 00:00:00 2001 From: Clemens Creutzburg Date: Sun, 19 Jul 2026 22:42:12 +0200 Subject: [PATCH] Kaffeeliste-Ausdruck: adaptives Layout nach Mitgliederzahl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der PDF-Export war bisher immer zweiseitig (Vieltrinker-/Wenigtrinker) mit fester Mindestzeilenzahl, unabhängig von der tatsächlichen Mitgliederzahl. Umgestellt auf: - Bis 49 aktive Mitglieder: eine Seite ohne Trennung. - Ab 50 Mitgliedern: weiterhin zwei Seiten (Vorder-/Rückseite). - Zeilenhöhe richtet sich nach der Mitgliederzahl (16-40px), statt fest 16px für alle. - Neue Mandanten-Einstellungen: freie Zeilen für neue Mitglieder an/aus, Trennmodus bei zwei Seiten (Trinkverhalten oder alphabetisch). Migration 0015 ergänzt die dafür nötigen tenant_settings-Spalten. Gegen die Dev-DB verifiziert: 8 Mitglieder -> einseitiges PDF, 53 Mitglieder (temporäre Testdaten) -> zweiseitiges PDF, Einstellungen inkl. Validierung geprüft. --- app/saas-auth.php | 22 ++- .../0015_saas_pdf_export_settings.sql | 3 + docs/backlog-druck-und-landingpage.md | 40 ++-- docs/m6-import-export-mail.md | 26 +++ exportKaffeeliste.php | 181 ++++++++++++------ mandant-einstellungen.php | 13 ++ 6 files changed, 193 insertions(+), 92 deletions(-) create mode 100644 database/migrations/0015_saas_pdf_export_settings.sql diff --git a/app/saas-auth.php b/app/saas-auth.php index 7a2a4d1..a08ff54 100644 --- a/app/saas-auth.php +++ b/app/saas-auth.php @@ -273,7 +273,9 @@ function saas_fetch_tenant_settings(PDO $pdo, int $tenantId): ?array ts.paypal_enabled, ts.paypal_url_template, ts.sheet_window_days, - ts.negative_warning_cents + ts.negative_warning_cents, + ts.pdf_show_empty_rows, + ts.pdf_split_mode FROM tenants t LEFT JOIN tenant_settings ts ON ts.tenant_id = t.id WHERE t.id = ? @@ -306,6 +308,8 @@ function saas_fetch_tenant_settings(PDO $pdo, int $tenantId): ?array 'paypal_url_template' => (string)$settings['paypal_url_template'], 'sheet_window_days' => (int)$settings['sheet_window_days'], 'negative_warning_cents' => (int)$settings['negative_warning_cents'], + 'pdf_show_empty_rows' => (int)$settings['pdf_show_empty_rows'], + 'pdf_split_mode' => (string)$settings['pdf_split_mode'], ]; } @@ -319,6 +323,7 @@ function saas_update_tenant_settings(PDO $pdo, int $tenantId, array $input): arr $sheetWindowDays = filter_var($input['sheet_window_days'] ?? null, FILTER_VALIDATE_INT); $negativeWarningCents = saas_parse_money_cents($input['negative_warning'] ?? ''); $paypalUrlTemplate = trim((string)($input['paypal_url_template'] ?? '')); + $pdfSplitMode = trim((string)($input['pdf_split_mode'] ?? 'drinking_behavior')); $errors = []; if (strlen($tenantName) < 3 || strlen($tenantName) > 255) { @@ -345,6 +350,9 @@ function saas_update_tenant_settings(PDO $pdo, int $tenantId, array $input): arr if (strlen($paypalUrlTemplate) > 1000) { $errors[] = 'Der PayPal-Link ist zu lang.'; } + if (!in_array($pdfSplitMode, ['drinking_behavior', 'alphabetical'], true)) { + $errors[] = 'Die Sortierung für den Ausdruck ist ungültig.'; + } if ($errors !== []) { return ['ok' => false, 'errors' => $errors]; @@ -352,6 +360,7 @@ function saas_update_tenant_settings(PDO $pdo, int $tenantId, array $input): arr $selfEntryEnabled = !empty($input['self_entry_enabled']) ? 1 : 0; $paypalEnabled = !empty($input['paypal_enabled']) ? 1 : 0; + $pdfShowEmptyRows = !empty($input['pdf_show_empty_rows']) ? 1 : 0; try { $pdo->beginTransaction(); @@ -366,15 +375,18 @@ function saas_update_tenant_settings(PDO $pdo, int $tenantId, array $input): arr $stmt = $pdo->prepare( 'INSERT INTO tenant_settings (tenant_id, mark_price_cents, self_entry_enabled, paypal_enabled, - paypal_url_template, sheet_window_days, negative_warning_cents) - VALUES (?, ?, ?, ?, ?, ?, ?) + paypal_url_template, sheet_window_days, negative_warning_cents, + pdf_show_empty_rows, pdf_split_mode) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE mark_price_cents = VALUES(mark_price_cents), self_entry_enabled = VALUES(self_entry_enabled), paypal_enabled = VALUES(paypal_enabled), paypal_url_template = VALUES(paypal_url_template), sheet_window_days = VALUES(sheet_window_days), - negative_warning_cents = VALUES(negative_warning_cents)' + negative_warning_cents = VALUES(negative_warning_cents), + pdf_show_empty_rows = VALUES(pdf_show_empty_rows), + pdf_split_mode = VALUES(pdf_split_mode)' ); $stmt->execute([ $tenantId, @@ -384,6 +396,8 @@ function saas_update_tenant_settings(PDO $pdo, int $tenantId, array $input): arr $paypalUrlTemplate, (int)$sheetWindowDays, -abs($negativeWarningCents), + $pdfShowEmptyRows, + $pdfSplitMode, ]); $pdo->commit(); diff --git a/database/migrations/0015_saas_pdf_export_settings.sql b/database/migrations/0015_saas_pdf_export_settings.sql new file mode 100644 index 0000000..bc064b6 --- /dev/null +++ b/database/migrations/0015_saas_pdf_export_settings.sql @@ -0,0 +1,3 @@ +ALTER TABLE tenant_settings + ADD COLUMN pdf_show_empty_rows TINYINT(1) NOT NULL DEFAULT 1 AFTER negative_warning_cents, + ADD COLUMN pdf_split_mode VARCHAR(20) NOT NULL DEFAULT 'drinking_behavior' AFTER pdf_show_empty_rows; diff --git a/docs/backlog-druck-und-landingpage.md b/docs/backlog-druck-und-landingpage.md index d11cc5f..3f83ac7 100644 --- a/docs/backlog-druck-und-landingpage.md +++ b/docs/backlog-druck-und-landingpage.md @@ -22,35 +22,21 @@ Noch nicht entschieden/umgesetzt. ## Kaffeeliste-Ausdruck (PDF-Export) als zentrale Funktion -Der PDF-Ausdruck (`exportKaffeeliste.php`, siehe -`docs/m6-import-export-mail.md` für den aktuellen Umsetzungsstand) soll -zu einer zentralen, prominent beworbenen Funktion werden – aktuell fixe -zweiseitige Vieltrinker-/Wenigtrinker-Ausgabe mit fester Mindestzeilenzahl -(63/64), ohne Rücksicht auf die tatsächliche Mitgliederzahl. +**Erledigt (2026-07-19)**, siehe `docs/m6-import-export-mail.md` Abschnitt +„Nachtrag: Adaptives Layout nach Mitgliederzahl" für Details: -Neue Anforderungen: +- ✅ Adaptives Layout nach Mitgliederanzahl (Zeilenhöhe 16–40px je nach + Mitgliederzahl). +- ✅ Vorder-/Rückseite bzw. mehrere Seiten erst ab 50 Mitgliedern + (`EXPORT_MULTI_PAGE_THRESHOLD`), darunter eine einseitige Liste. +- ✅ Admin-Einstellung „Freie Zeilen für neue Mitglieder" + (`tenant_settings.pdf_show_empty_rows`, Formular in + `mandant-einstellungen.php`). +- ✅ Trennmodus bei mehreren Seiten wählbar: Trinkverhalten oder + alphabetisch (`tenant_settings.pdf_split_mode`). -- **Adaptives Layout nach Mitgliederanzahl.** Bis 50 Mitglieder passen - auf eine Seite; die Zeilenhöhe/-größe soll sich an der Mitgliederzahl - orientieren (weniger Mitglieder → größere, besser lesbare Zeilen statt - fester Zeilenhöhe wie bisher). -- **Vorder-/Rückseite bzw. mehrere Seiten erst ab 50 Mitgliedern.** - Unterhalb dieser Schwelle reicht eine einseitige Liste; die bestehende - Vieltrinker-/Wenigtrinker-Aufteilung auf zwei Seiten wird zur Option ab - 50 Mitgliedern, nicht mehr Standard für alle Mandanten. -- **Admin-Einstellung: freie Zeilen für neue Mitglieder.** Der Admin soll - konfigurieren können, ob die Liste zusätzliche leere Zeilen für neue - Mitglieder enthält (aktuelles Verhalten: Auffüllen bis `minRows`) oder - nur die tatsächlich vorhandenen Mitglieder zeigt. -- **Trennmodus bei mehreren Seiten wählbar.** Zur Auswahl: Trennung nach - Trinkverhalten (wie bisher: vorne Vieltrinker, hinten Wenigtrinker) oder - einfach alphabetisch über alle Seiten. - -Technisch vermutlich als neue Spalten in `tenant_settings` -(vgl. `database/migrations/0002_saas_identity_tenants.sql`) abzubilden, -z. B. `pdf_show_empty_rows`, `pdf_multi_page_threshold`, -`pdf_split_mode` (`drinking_behavior` | `alphabetical`) – noch nicht -entschieden, nur als naheliegender Ansatzpunkt notiert. +Die Kaffeeliste als zentrale, prominent beworbene Funktion auf der +Landingpage darzustellen (siehe unten) ist noch offen. ### RFID-Kartenerfassung (zukünftig, nicht öffentlich bewerben) diff --git a/docs/m6-import-export-mail.md b/docs/m6-import-export-mail.md index c514175..37f2eb1 100644 --- a/docs/m6-import-export-mail.md +++ b/docs/m6-import-export-mail.md @@ -99,6 +99,32 @@ Live getestet: gültiges zweiseitiges PDF (58 KB) mit korrektem stichprobenartig verifiziert (unkomprimierte Testausgabe), Vieltrinker-/ Wenigtrinker-Aufteilung stimmt mit den Ledger-Daten überein. +### Nachtrag: Adaptives Layout nach Mitgliederzahl (2026-07-19) + +Der Export war bis dahin immer zweiseitig mit fester Mindestzeilenzahl +(63/64 Zeilen a 16px), unabhängig von der tatsächlichen Mitgliederzahl. +Umgestellt auf: + +- Bis 49 aktive Mitglieder: eine Seite, keine Vieltrinker-/Wenigtrinker- + Trennung. Ab 50 (`EXPORT_MULTI_PAGE_THRESHOLD` in + `exportKaffeeliste.php`): zwei Seiten wie bisher. +- Zeilenhöhe wird aus einem festen Platzbudget pro Seite errechnet + (`export_row_height_px()`), begrenzt auf 16–40px – weniger Mitglieder + ergeben größere statt winziger Zeilen. +- Neue Mandanten-Einstellungen (`tenant_settings.pdf_show_empty_rows`, + `tenant_settings.pdf_split_mode`, Migration + `0015_saas_pdf_export_settings.sql`, Formular in + `mandant-einstellungen.php`): Admin legt fest, ob freie Zeilen für neue + Mitglieder aufgefüllt werden, und ob die Zweiseiten-Trennung nach + Trinkverhalten (wie bisher) oder alphabetisch erfolgt. +- Verifiziert gegen die Dev-DB: 8 aktive Mitglieder ergeben ein + einseitiges PDF (`/Count 1`), 53 aktive Mitglieder ein zweiseitiges + (`/Count 2`); `saas_update_tenant_settings()` speichert und validiert + die neuen Felder korrekt (ungültiger `pdf_split_mode` wird abgelehnt). +- RFID-Kartenerfassung als künftige Erfassungsmethode ist weiterhin nur + im Backlog vermerkt (`docs/backlog-druck-und-landingpage.md`), nicht + umgesetzt und nicht beworben. + ## Mailversand Umgesetzte Dateien: diff --git a/exportKaffeeliste.php b/exportKaffeeliste.php index 977403e..dfac242 100644 --- a/exportKaffeeliste.php +++ b/exportKaffeeliste.php @@ -29,9 +29,88 @@ if (!$hasAccess) { $settings = saas_fetch_tenant_settings($pdo, $tenantId); $windowDays = $settings['sheet_window_days'] ?? 100; $markPrice = number_format(($settings['mark_price_cents'] ?? 20) / 100, 2, ',', '.'); +$showEmptyRows = (int)($settings['pdf_show_empty_rows'] ?? 1) === 1; +$splitMode = (string)($settings['pdf_split_mode'] ?? 'drinking_behavior'); -$vieltrinker = ledger_fetch_participants_by_window_marks($pdo, $tenantId, $windowDays, true); -$wenigtrinker = ledger_fetch_participants_by_window_marks($pdo, $tenantId, $windowDays, false); +// 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). +define('EXPORT_PAGE_ROW_BUDGET_PX', 63 * 16); +define('EXPORT_MIN_ROW_HEIGHT_PX', 16); +define('EXPORT_MAX_ROW_HEIGHT_PX', 40); +// Ziel-Zeilenzahl je Seite im Zweiseiten-Modus, wenn freie Zeilen erwuenscht +// sind - identisch zur bisherigen, getesteten Dichte (63 Zeilen a 16px). +define('EXPORT_MULTI_PAGE_FILL_ROWS', 63); + +$allActive = ledger_fetch_participant_summaries($pdo, $tenantId, ['active_only' => true]); +$memberCount = count($allActive); +$isMultiPage = $memberCount >= EXPORT_MULTI_PAGE_THRESHOLD; + +function export_row_height_px(int $targetRows): float +{ + $height = EXPORT_PAGE_ROW_BUDGET_PX / max(1, $targetRows); + + return max(EXPORT_MIN_ROW_HEIGHT_PX, min(EXPORT_MAX_ROW_HEIGHT_PX, $height)); +} + +/** + * Rendert eine Teilnehmerseite als HTML-Tabelle fuer TCPDF. Die Zeilenhoehe + * richtet sich nach der Ziel-Zeilenzahl (Mitglieder plus ggf. Fuellzeilen), + * damit wenige Mitglieder groessere statt winziger Zeilen ergeben. + * + * @param list $teilnehmer + */ +function export_render_page_html(array $teilnehmer, string $titel, string $hinweisText, string $markPrice, int $targetRows, bool $showEmptyRows, ?string $seitenHinweis): string +{ + $rowHeight = export_row_height_px($showEmptyRows ? $targetRows : max(1, count($teilnehmer))); + + $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'); @@ -52,55 +131,6 @@ class MyCustomPDFWithWatermark extends TCPDF { } } -/** - * Rendert eine Teilnehmerseite (Vieltrinker/Wenigtrinker) als HTML-Tabelle - * fuer TCPDF, inklusive Fuellzeilen bis zur Mindesthoehe. - * - * @param list $teilnehmer - */ -function export_render_page_html(array $teilnehmer, string $titel, string $hinweisText, string $markPrice, int $minRows, string $rueckseitenHinweis): string -{ - $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 .= ""; - } - - for ($i = $rowCount; $i < $minRows; $i++) { - $html .= ''; - } - $html .= ''; - $html .= '
' . $titel . '1 Strich = ' . $markPrice . '€. Bitte bezahlen bei 10 € zahlen. ' . date("d.m.Y H:s") . '
NameGuthabenStriche
{$name}' . $balance . ' €' . $balance . ' €{$balance} €
   
  ' . $rueckseitenHinweis . '
'; - - return $html; -} - $pdf = new MyCustomPDFWithWatermark(PDF_PAGE_ORIENTATION, 'mm', 'A4', true, 'UTF-8', false); $pdf->SetHeaderData("", 0, "Kaffeestrichliste", ""); $pdf->SetPrintFooter(false); @@ -108,16 +138,45 @@ $pdf->SetMargins('5', '5', '5'); $pdf->SetAutoPageBreak(true, 5); $pdf->SetFont('helvetica', '', 9.5); -$pdf->AddPage(); -$pdf->writeHTML( - export_render_page_html($vieltrinker, 'Kaffeeliste - Vieltrinker', 'background-color: rgb(0, 94, 63);color: rgb(255, 255, 255);', $markPrice, 63, 'Rückseite beachten!'), - true, false, true, false, '' -); +if (!$isMultiPage) { + $pdf->AddPage(); + $pdf->writeHTML( + export_render_page_html( + $allActive, + 'Kaffeeliste', + 'background-color: rgb(0, 94, 63);color: rgb(255, 255, 255);', + $markPrice, + EXPORT_MULTI_PAGE_THRESHOLD, + $showEmptyRows, + null + ), + true, false, true, false, '' + ); +} else { + if ($splitMode === 'alphabetical') { + $mid = (int)ceil($memberCount / 2); + $front = array_slice($allActive, 0, $mid); + $back = array_slice($allActive, $mid); + $frontTitel = 'Kaffeeliste – Teil 1'; + $backTitel = 'Kaffeeliste – Teil 2'; + } else { + $front = ledger_fetch_participants_by_window_marks($pdo, $tenantId, $windowDays, true); + $back = ledger_fetch_participants_by_window_marks($pdo, $tenantId, $windowDays, false); + $frontTitel = 'Kaffeeliste - Vieltrinker'; + $backTitel = 'Kaffeeliste - Wenigtrinker'; + } -$pdf->AddPage(); -$pdf->writeHTML( - export_render_page_html($wenigtrinker, 'Kaffeeliste - Wenigtrinker', 'background-color: rgb(91, 209, 215);color: rgb(0, 0, 0);', $markPrice, 64, 'Vorderseite beachten!'), - true, false, true, false, '' -); + $pdf->AddPage(); + $pdf->writeHTML( + export_render_page_html($front, $frontTitel, 'background-color: rgb(0, 94, 63);color: rgb(255, 255, 255);', $markPrice, EXPORT_MULTI_PAGE_FILL_ROWS, $showEmptyRows, 'Rückseite beachten!'), + true, false, true, false, '' + ); + + $pdf->AddPage(); + $pdf->writeHTML( + export_render_page_html($back, $backTitel, 'background-color: rgb(91, 209, 215);color: rgb(0, 0, 0);', $markPrice, EXPORT_MULTI_PAGE_FILL_ROWS, $showEmptyRows, 'Vorderseite beachten!'), + true, false, true, false, '' + ); +} $pdf->Output('Kaffeestrichliste.pdf', 'D'); diff --git a/mandant-einstellungen.php b/mandant-einstellungen.php index b05fc8d..e40743a 100644 --- a/mandant-einstellungen.php +++ b/mandant-einstellungen.php @@ -32,6 +32,8 @@ if ($canManageSettings) { 'paypal_url_template' => $_POST['paypal_url_template'] ?? '', 'sheet_window_days' => $_POST['sheet_window_days'] ?? 100, 'negative_warning_cents' => -abs(saas_parse_money_cents($_POST['negative_warning'] ?? '') ?? 0), + 'pdf_show_empty_rows' => !empty($_POST['pdf_show_empty_rows']) ? 1 : 0, + 'pdf_split_mode' => $_POST['pdf_split_mode'] ?? 'drinking_behavior', ]; } } else { @@ -114,6 +116,17 @@ include 'nav.php'; > +
+ > + +
+
+ + +