From dbddee779113f9f93830d14b029d13702b361732 Mon Sep 17 00:00:00 2001 From: Eduard Wisch Date: Thu, 9 Apr 2026 09:10:56 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Block=20C=20=E2=80=94=20Editor-Polish,?= =?UTF-8?q?=20Vorher/Nachher,=20Versionierung,=20Batch-Modus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Desktop-Editor Polish: - Aktives Seiten-Thumbnail deutlich markiert (scale + shadow + blauer Label-Hintergrund) - Titel-Feld pro Seite in der DB (llx_bericht_page.title) Phase 5.2 Vorher/Nachher-Layout: - Neuer layout-Typ 'before_after' in bericht_render_page_to_pdf - Zwei Bilder nebeneinander, Labels 'Vorher' / 'Nachher', Titel oben - 'VN'-Button in der Anhänge-Grid-Leiste - create_grid_page akzeptiert before_after - Layout-Dropdown hat before_after + title_only Option Phase 5.2b Title-Only Seiten: - Reine Titel/Zwischentitel-Seiten ohne Bild - 32pt Titel zentriert, optional Notiz darunter - bericht_render_page_to_pdf behandelt title_only separat Phase 5.3 Bericht-Versionierung: - Neue Spalten version + fk_bericht_parent in llx_bericht - Bericht::duplicateAsNewVersion() kopiert alles inkl. Seiten - '🔀 Neue Version'-Button im Editor-Footer - Versions-Links in der Meta-Zeile (v1, v2, v3 …) mit Sprungmöglichkeit - Alte Versionen bleiben unverändert erhalten Phase 5.6 Batch-Modus: - Neue Seite bericht_batch.php mit Filter (Datum von/bis, Suche) - Checkbox-Liste aller finalisierten Berichte - 'Ausgewählte als Sammel-PDF herunterladen' → FPDI merged alle final_pdf_path in ein neues PDF mit Inhaltsverzeichnis-Seite - Link im Admin-Setup api/pages.php: - POST-Update akzeptiert jetzt auch title und layout zusätzlich zu note und rotation Co-Authored-By: Claude Opus 4.6 (1M context) [deploy] --- admin/setup.php | 8 ++ ajax/create_grid_page.php | 2 +- api/pages.php | 12 ++- bericht_batch.php | 173 ++++++++++++++++++++++++++++++ bericht_card.php | 35 +++++- class/bericht.class.php | 79 ++++++++++++-- core/modules/modBericht.class.php | 5 + css/bericht.css | 8 +- lib/bericht.lib.php | 78 +++++++++++++- 9 files changed, 386 insertions(+), 14 deletions(-) create mode 100644 bericht_batch.php diff --git a/admin/setup.php b/admin/setup.php index da17ad5..373d94a 100644 --- a/admin/setup.php +++ b/admin/setup.php @@ -105,6 +105,14 @@ print '

Mit dem Handy print ''; print ''; +// Batch-Modus Link +print '

'; +print '

📦 Batch-Modus

'; +print '

Mehrere finalisierte Berichte zu einem Sammel-PDF zusammenführen (z. B. für Monatsabschluss, Jahresarchiv).

'; +$batch_url = dol_buildpath('/bericht/bericht_batch.php', 1); +print '📦 Zum Batch-Tool'; +print '
'; + // API-Status print '
'; print '

🔌 REST-API Status

'; diff --git a/ajax/create_grid_page.php b/ajax/create_grid_page.php index 5a8cf1c..d2fb63f 100644 --- a/ajax/create_grid_page.php +++ b/ajax/create_grid_page.php @@ -14,7 +14,7 @@ $relpaths_raw = (string) ($_POST['relpaths'] ?? '[]'); $relpaths = json_decode($relpaths_raw, true); if (!is_array($relpaths) || empty($relpaths)) bericht_ajax_fail('Keine Bilder ausgewählt'); -$valid_layouts = array('grid_2', 'grid_2v', 'grid_4', 'grid_6'); +$valid_layouts = array('grid_2', 'grid_2v', 'grid_4', 'grid_6', 'before_after'); if (!in_array($layout, $valid_layouts, true)) $layout = 'grid_4'; $bericht = new Bericht($db); diff --git a/api/pages.php b/api/pages.php index 29ae2c0..9ff7c60 100644 --- a/api/pages.php +++ b/api/pages.php @@ -213,7 +213,7 @@ if ($method === 'DELETE' || ($method === 'POST' && ($_GET['delete'] ?? '') === ' api_ok(); } -/* ---------- UPDATE page (note, rotation) ---------- */ +/* ---------- UPDATE page (note, title, rotation, layout) ---------- */ if ($method === 'POST') { $in = api_input(); $sets = array(); @@ -221,11 +221,21 @@ if ($method === 'POST') { $note = (string) $in['note']; $sets[] = "note = ".($note !== '' ? "'".$db->escape($note)."'" : "NULL"); } + if (isset($in['title'])) { + $title = (string) $in['title']; + $sets[] = "title = ".($title !== '' ? "'".$db->escape($title)."'" : "NULL"); + } if (isset($in['rotation'])) { $rot = (int) $in['rotation']; $rot = (($rot % 360) + 360) % 360; $sets[] = "rotation = ".$rot; } + if (isset($in['layout'])) { + $valid = array('single','grid_2','grid_2v','grid_4','grid_6','before_after','title_only'); + if (in_array($in['layout'], $valid, true)) { + $sets[] = "layout = '".$db->escape($in['layout'])."'"; + } + } if (empty($sets)) api_fail('Nichts zu aktualisieren'); $sql = "UPDATE ".$db->prefix()."bericht_page SET ".implode(',', $sets)." WHERE rowid = ".$page_id; diff --git a/bericht_batch.php b/bericht_batch.php new file mode 100644 index 0000000..07e2f6e --- /dev/null +++ b/bericht_batch.php @@ -0,0 +1,173 @@ + 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) { $i--; $j--; } +if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) $res = @include substr($tmp, 0, ($i + 1))."/main.inc.php"; +if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) $res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php"; +if (!$res && file_exists("../main.inc.php")) $res = @include "../main.inc.php"; +if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php"; +if (!$res) die("Include of main fails"); + +require_once __DIR__.'/class/bericht.class.php'; +require_once __DIR__.'/lib/bericht.lib.php'; + +if (!$user->hasRight('bericht', 'read')) accessforbidden(); + +$langs->loadLangs(array("bericht@bericht", "main")); + +$action = GETPOST('action', 'alpha'); +$datefrom = GETPOST('datefrom', 'alpha'); +$dateto = GETPOST('dateto', 'alpha'); +$q = GETPOST('q', 'alpha'); + +/* ---------- Batch-PDF generieren ---------- */ +if ($action === 'generate') { + $ids = GETPOST('ids', 'array'); + if (empty($ids)) { + setEventMessages('Keine Berichte ausgewählt', null, 'errors'); + header("Location: ".$_SERVER['PHP_SELF']); exit; + } + + // TCPDF + FPDI laden + $tcpdf_loaded = false; + foreach (array( + DOL_DOCUMENT_ROOT.'/includes/tecnickcom/tcpdf/tcpdf.php', + DOL_DOCUMENT_ROOT.'/includes/tcpdf/tcpdf.php', + ) as $p) { if (file_exists($p)) { require_once $p; $tcpdf_loaded = true; break; } } + if (!$tcpdf_loaded) { http_response_code(500); exit('TCPDF fehlt'); } + + $fpdi_loaded = false; + foreach (array( + DOL_DOCUMENT_ROOT.'/includes/setasign/vendor/setasign/fpdi/src/Tcpdf/Fpdi.php', + DOL_DOCUMENT_ROOT.'/includes/fpdi/src/Tcpdf/Fpdi.php', + ) as $p) { if (file_exists($p)) { require_once $p; $fpdi_loaded = true; break; } } + if (!$fpdi_loaded) { http_response_code(500); exit('FPDI wird für Batch-Modus benötigt'); } + + $pdf = new \setasign\Fpdi\Tcpdf\Fpdi('P', 'mm', 'A4', true, 'UTF-8', false); + $pdf->SetCreator('Dolibarr Bericht-Modul Batch'); + $pdf->SetAuthor($user->getFullName($langs)); + $pdf->SetTitle('Bericht-Sammlung '.dol_print_date(dol_now(), '%Y-%m-%d')); + $pdf->setPrintHeader(false); + $pdf->setPrintFooter(false); + + // Inhaltsverzeichnis-Seite + $pdf->AddPage('P', 'A4'); + $pdf->SetFont('helvetica', 'B', 18); + $pdf->Cell(0, 12, 'Bericht-Sammlung', 0, 1, 'C'); + $pdf->SetFont('helvetica', '', 11); + $pdf->Cell(0, 8, 'Erstellt: '.dol_print_date(dol_now(), 'dayhour'), 0, 1, 'C'); + $pdf->Ln(8); + $pdf->SetFont('helvetica', 'B', 13); + $pdf->Cell(0, 8, 'Enthaltene Berichte ('.count($ids).')', 0, 1, 'L'); + $pdf->SetFont('helvetica', '', 10); + + $berichte = array(); + foreach ($ids as $id) { + $b = new Bericht($db); + if ($b->fetch((int) $id) > 0 && $b->status == Bericht::STATUS_FINAL && !empty($b->final_pdf_path)) { + $pp = bericht_resolve_data_path($b->final_pdf_path); + if ($pp && file_exists($pp)) { + $berichte[] = array('bericht' => $b, 'pdf' => $pp); + $pdf->Cell(0, 6, '• '.$b->ref.' – '.($b->titel ?: '').' ('.$b->auftragsnummer.')', 0, 1, 'L'); + } + } + } + + if (empty($berichte)) { + exit('Keine gültigen (finalisierten) Berichte in der Auswahl'); + } + + // Jede Bericht-PDF als volle Seiten einbauen + foreach ($berichte as $entry) { + try { + $page_count = $pdf->setSourceFile($entry['pdf']); + for ($n = 1; $n <= $page_count; $n++) { + $tpl = $pdf->importPage($n); + $size = $pdf->getTemplateSize($tpl); + $pdf->AddPage($size['orientation'] ?? 'P', array($size['width'], $size['height'])); + $pdf->useTemplate($tpl); + } + } catch (Throwable $e) { continue; } + } + + $filename = 'Bericht-Sammlung_'.dol_print_date(dol_now(), '%Y%m%d_%H%M%S').'.pdf'; + header('Content-Type: application/pdf'); + header('Content-Disposition: attachment; filename="'.$filename.'"'); + $pdf->Output($filename, 'D'); + exit; +} + +/* ---------- Listen-Ansicht ---------- */ +llxHeader('', 'Bericht-Batch-Modus'); +print load_fiche_titre('📦 Bericht-Batch — Mehrere Berichte zu einem PDF', '', 'bill'); + +// Filter +print '
'; +print '   '; +print '   '; +print '   '; +print ''; +print '
'; + +// Query: nur finale Berichte mit PDF, gefiltert +$where = "status = 1 AND final_pdf_path IS NOT NULL AND COALESCE(is_template,0) = 0 AND entity IN (".getEntity('bericht').")"; +if ($datefrom) { + $where .= " AND datec >= '".$db->escape($datefrom." 00:00:00")."'"; +} +if ($dateto) { + $where .= " AND datec <= '".$db->escape($dateto." 23:59:59")."'"; +} +if ($q) { + $qe = $db->escape($q); + $where .= " AND (ref LIKE '%$qe%' OR titel LIKE '%$qe%' OR auftragsnummer LIKE '%$qe%')"; +} + +$sql = "SELECT rowid, ref, titel, auftragsnummer, element_type, fk_element, datec FROM ".$db->prefix()."bericht WHERE ".$where." ORDER BY datec DESC LIMIT 500"; +$resq = $db->query($sql); + +print '
'; +print ''; +print ''; +print ''; +print ''; +print ''; +print ''; +print ''; +print ''; +print ''; +print ''; + +$count = 0; +if ($resq) { + while ($o = $db->fetch_object($resq)) { + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + $count++; + } +} +print '
RefTitelAuftragsnummerDatum
'.dol_escape_htmltag($o->ref).''.dol_escape_htmltag($o->titel).''.dol_escape_htmltag($o->auftragsnummer).''.dol_print_date($db->jdate($o->datec), 'dayhour').'
'; + +if ($count === 0) { + print '

Keine finalisierten Berichte gefunden.

'; +} else { + print '
'; + print ''; + print '
'; +} +print '
'; + +llxFooter(); +$db->close(); diff --git a/bericht_card.php b/bericht_card.php index d0d3da7..33fb827 100644 --- a/bericht_card.php +++ b/bericht_card.php @@ -103,6 +103,17 @@ if ($action === 'create' && $user->hasRight('bericht', 'write')) { setEventMessages($b->error, $b->errors, 'errors'); } +// Aktion: neue Version erstellen +if ($action === 'new_version' && $berichtid > 0 && $user->hasRight('bericht', 'write')) { + $new_id = $bericht->duplicateAsNewVersion($user); + if ($new_id) { + setEventMessages('Neue Version erstellt', null, 'mesgs'); + header("Location: ".$_SERVER['PHP_SELF'].'?berichtid='.$new_id); + exit; + } + setEventMessages('Versionierung fehlgeschlagen', null, 'errors'); +} + // Aktion: Bericht löschen if ($action === 'delete' && $berichtid > 0 && $user->hasRight('bericht', 'delete')) { if ($bericht->delete($user) > 0) { @@ -310,8 +321,24 @@ if (!$bericht) { print '
'; print ''; print ''; + // Versionsliste derselben Kette ermitteln + $chain_id = $bericht->fk_bericht_parent ?: $bericht->id; + $version_links = ''; + $vres = $db->query("SELECT rowid, version FROM ".$db->prefix()."bericht" + ." WHERE rowid = ".((int) $chain_id)." OR fk_bericht_parent = ".((int) $chain_id) + ." ORDER BY version ASC"); + if ($vres) { + $vs = array(); + while ($vr = $db->fetch_object($vres)) { + $active = ((int) $vr->rowid === (int) $bericht->id); + $vs[] = 'v'.$vr->version.''; + } + if (count($vs) > 1) $version_links = '   Versionen: '.implode(' · ', $vs).''; + } + print ''; - print ''; + print ''; print ''; print ''; print ''; @@ -382,6 +409,7 @@ if (!$bericht) { print ' '; print ' '; print ''; + print ' '; print ''; } print '
'; @@ -442,6 +470,8 @@ if (!$bericht) { .'' .'' .'' + .'' + .'' .''; print '
'.$langs->trans("Ref").''.dol_escape_htmltag($bericht->ref).''.$langs->trans("Ref").''.dol_escape_htmltag($bericht->ref).' v'.((int) $bericht->version).''.$version_links.''.$langs->trans("BerichtAuftragsnummer").''.dol_escape_htmltag($bericht->auftragsnummer).'
'.$langs->trans("BerichtTitle").'