feat: Block C — Editor-Polish, Vorher/Nachher, Versionierung, Batch-Modus
All checks were successful
Deploy bericht / deploy (push) Successful in 1s
All checks were successful
Deploy bericht / deploy (push) Successful in 1s
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) <noreply@anthropic.com>
[deploy]
This commit is contained in:
parent
1705744809
commit
dbddee7791
9 changed files with 386 additions and 14 deletions
|
|
@ -105,6 +105,14 @@ print ' <p style="color:#e0e8f0;font-size:12px;margin:12px 0 0;">Mit dem Handy
|
|||
print '</div>';
|
||||
print '</div>';
|
||||
|
||||
// Batch-Modus Link
|
||||
print '<div class="bericht-setup-section">';
|
||||
print '<h3>📦 Batch-Modus</h3>';
|
||||
print '<p>Mehrere finalisierte Berichte zu einem Sammel-PDF zusammenführen (z. B. für Monatsabschluss, Jahresarchiv).</p>';
|
||||
$batch_url = dol_buildpath('/bericht/bericht_batch.php', 1);
|
||||
print '<a href="'.dol_escape_htmltag($batch_url).'" class="butAction">📦 Zum Batch-Tool</a>';
|
||||
print '</div>';
|
||||
|
||||
// API-Status
|
||||
print '<div class="bericht-setup-section">';
|
||||
print '<h3>🔌 REST-API Status</h3>';
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
173
bericht_batch.php
Normal file
173
bericht_batch.php
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
<?php
|
||||
/* Batch-Modus: mehrere Berichte zu einem Sammel-PDF zusammenführen.
|
||||
* Nur finale Berichte mit existierendem final_pdf_path werden berücksichtigt.
|
||||
*
|
||||
* GET: filter per Datum-Von/Bis, Element-Typ, Kundensuche, Status
|
||||
* POST: action=generate + ids[] → liefert ein zusammengesetztes PDF aus den ausgewählten
|
||||
*/
|
||||
|
||||
$res = 0;
|
||||
if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) $res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php";
|
||||
$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME']; $tmp2 = realpath(__FILE__); $i = strlen($tmp) - 1; $j = strlen($tmp2) - 1;
|
||||
while ($i > 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 '<form method="get" class="bericht-batch-filter" style="margin-bottom:16px;padding:12px;background:var(--colorbacktitle1,#f5f5f5);border-radius:6px;">';
|
||||
print '<label>Von: <input type="date" name="datefrom" value="'.dol_escape_htmltag($datefrom).'"></label> ';
|
||||
print '<label>Bis: <input type="date" name="dateto" value="'.dol_escape_htmltag($dateto).'"></label> ';
|
||||
print '<label>Suche: <input type="text" name="q" value="'.dol_escape_htmltag($q).'" placeholder="Ref, Titel, Auftragsnr"></label> ';
|
||||
print '<button type="submit" class="butAction">🔍 Filtern</button>';
|
||||
print '</form>';
|
||||
|
||||
// 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 '<form method="post" action="'.$_SERVER['PHP_SELF'].'">';
|
||||
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||
print '<input type="hidden" name="action" value="generate">';
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<th><input type="checkbox" onclick="document.querySelectorAll(\'.bt-check\').forEach(c => c.checked = this.checked)"></th>';
|
||||
print '<th>Ref</th>';
|
||||
print '<th>Titel</th>';
|
||||
print '<th>Auftragsnummer</th>';
|
||||
print '<th>Datum</th>';
|
||||
print '</tr>';
|
||||
|
||||
$count = 0;
|
||||
if ($resq) {
|
||||
while ($o = $db->fetch_object($resq)) {
|
||||
print '<tr class="oddeven">';
|
||||
print '<td><input type="checkbox" class="bt-check" name="ids[]" value="'.$o->rowid.'"></td>';
|
||||
print '<td>'.dol_escape_htmltag($o->ref).'</td>';
|
||||
print '<td>'.dol_escape_htmltag($o->titel).'</td>';
|
||||
print '<td>'.dol_escape_htmltag($o->auftragsnummer).'</td>';
|
||||
print '<td>'.dol_print_date($db->jdate($o->datec), 'dayhour').'</td>';
|
||||
print '</tr>';
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
print '</table>';
|
||||
|
||||
if ($count === 0) {
|
||||
print '<p class="opacitymedium">Keine finalisierten Berichte gefunden.</p>';
|
||||
} else {
|
||||
print '<div style="margin-top:16px;text-align:right;">';
|
||||
print '<button type="submit" class="butActionConfirm">📦 Ausgewählte als Sammel-PDF herunterladen</button>';
|
||||
print '</div>';
|
||||
}
|
||||
print '</form>';
|
||||
|
||||
llxFooter();
|
||||
$db->close();
|
||||
|
|
@ -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 '<form method="post" id="bericht-meta-form">';
|
||||
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||
print '<input type="hidden" name="berichtid" value="'.$bericht->id.'">';
|
||||
// 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[] = '<a href="'.$_SERVER['PHP_SELF'].'?berichtid='.$vr->rowid.'" '
|
||||
.($active ? 'style="font-weight:bold;text-decoration:underline;"' : '').'>v'.$vr->version.'</a>';
|
||||
}
|
||||
if (count($vs) > 1) $version_links = ' <span class="opacitymedium small">Versionen: '.implode(' · ', $vs).'</span>';
|
||||
}
|
||||
|
||||
print '<table class="border centpercent"><tr>';
|
||||
print '<td class="titlefield">'.$langs->trans("Ref").'</td><td>'.dol_escape_htmltag($bericht->ref).'</td>';
|
||||
print '<td class="titlefield">'.$langs->trans("Ref").'</td><td>'.dol_escape_htmltag($bericht->ref).' <span class="opacitymedium">v'.((int) $bericht->version).'</span>'.$version_links.'</td>';
|
||||
print '<td class="titlefield">'.$langs->trans("BerichtAuftragsnummer").'</td><td>'.dol_escape_htmltag($bericht->auftragsnummer).'</td>';
|
||||
print '</tr><tr>';
|
||||
print '<td>'.$langs->trans("BerichtTitle").'</td><td><input type="text" name="titel" value="'.dol_escape_htmltag($bericht->titel).'" size="40"></td>';
|
||||
|
|
@ -382,6 +409,7 @@ if (!$bericht) {
|
|||
print '<button type="button" class="btn-add-grid" data-layout="grid_2v" title="2 Bilder (links/rechts)">▯▯</button> ';
|
||||
print '<button type="button" class="btn-add-grid" data-layout="grid_4" title="2x2 Grid">▦</button> ';
|
||||
print '<button type="button" class="btn-add-grid" data-layout="grid_6" title="3x2 Grid">▦▦</button>';
|
||||
print ' <button type="button" class="btn-add-grid" data-layout="before_after" title="Vorher / Nachher (2 Bilder)">VN</button>';
|
||||
print '</div>';
|
||||
}
|
||||
print '<hr>';
|
||||
|
|
@ -442,6 +470,8 @@ if (!$bericht) {
|
|||
.'<option value="grid_2v">2 Bilder (links/rechts)</option>'
|
||||
.'<option value="grid_4">4 Bilder (2x2)</option>'
|
||||
.'<option value="grid_6">6 Bilder (3x2)</option>'
|
||||
.'<option value="before_after">Vorher / Nachher</option>'
|
||||
.'<option value="title_only">Nur Titel</option>'
|
||||
.'</select></label>';
|
||||
print '<label class="single-only" title="Wie groß das Bild auf der Seite gedruckt wird (nur Single)">Größe: <select id="page-imgscale">'
|
||||
.'<option value="1.0">Vollseite (100%)</option>'
|
||||
|
|
@ -509,6 +539,9 @@ if (!$bericht) {
|
|||
print '<button type="button" id="btn-save-draft" class="butAction" title="Aktuellen Stand als Entwurf speichern">💾 '.$langs->trans("BerichtSaveDraft").'</button>';
|
||||
print '<button type="button" id="btn-preview" class="butAction" title="PDF-Vorschau ansehen ohne zu finalisieren">👁️ Vorschau</button>';
|
||||
print '<button type="button" id="btn-save-as-template" class="butAction" title="Aktuellen Bericht als wiederverwendbare Vorlage speichern">📋 Als Vorlage</button>';
|
||||
print '<a href="'.$_SERVER['PHP_SELF'].'?action=new_version&berichtid='.$bericht->id.'&token='.newToken().'" '
|
||||
.'onclick="return confirm(\'Neue Version (v'.((int)$bericht->version + 1).') erstellen? Der aktuelle Bericht bleibt unverändert.\')" '
|
||||
.'class="butAction" title="Neue Version des Berichts anlegen, aktueller bleibt erhalten">🔀 Neue Version</a>';
|
||||
print '<button type="button" id="btn-finalize" class="butActionConfirm" title="PDF erzeugen und unter Verknüpfte Dokumente ablegen">📑 '.$langs->trans("BerichtFinalize").'</button>';
|
||||
if ($user->hasRight('bericht', 'delete')) {
|
||||
print '<a href="'.$_SERVER['PHP_SELF'].'?action=delete&berichtid='.$bericht->id.'&token='.newToken().'" '
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ class Bericht extends CommonObject
|
|||
public $page_orientation = 'P'; // P=Portrait, L=Landscape
|
||||
public $is_template = 0; // 1 = Vorlage, wird nicht als regulärer Bericht gelistet
|
||||
public $template_label; // Optional Label für die Vorlage
|
||||
public $version = 1; // Version-Nummer (nur bei versionierten Berichten)
|
||||
public $fk_bericht_parent; // Parent-Bericht bei Versionierung
|
||||
public $status; // 0 = Entwurf, 1 = Final
|
||||
public $final_pdf_path;
|
||||
public $fk_user_creat;
|
||||
|
|
@ -58,7 +60,7 @@ class Bericht extends CommonObject
|
|||
}
|
||||
|
||||
$sql = "INSERT INTO ".$this->db->prefix()."bericht ("
|
||||
."entity, ref, titel, element_type, fk_element, auftragsnummer, template_odt, page_format, page_orientation, is_template, template_label, status, fk_user_creat, datec, note"
|
||||
."entity, ref, titel, element_type, fk_element, auftragsnummer, template_odt, page_format, page_orientation, is_template, template_label, version, fk_bericht_parent, status, fk_user_creat, datec, note"
|
||||
.") VALUES ("
|
||||
.((int) $this->entity).","
|
||||
."'".$this->db->escape($this->ref)."',"
|
||||
|
|
@ -71,6 +73,8 @@ class Bericht extends CommonObject
|
|||
."'".$this->db->escape($this->page_orientation)."',"
|
||||
.((int) ($this->is_template ? 1 : 0)).","
|
||||
.($this->template_label ? "'".$this->db->escape($this->template_label)."'" : "NULL").","
|
||||
.((int) ($this->version ?: 1)).","
|
||||
.($this->fk_bericht_parent ? (int) $this->fk_bericht_parent : "NULL").","
|
||||
.((int) $this->status).","
|
||||
.((int) $this->fk_user_creat).","
|
||||
."'".$this->db->idate($this->datec)."',"
|
||||
|
|
@ -93,6 +97,7 @@ class Bericht extends CommonObject
|
|||
{
|
||||
$sql = "SELECT rowid, entity, ref, titel, element_type, fk_element, auftragsnummer, template_odt,"
|
||||
." page_format, page_orientation, COALESCE(is_template,0) AS is_template, template_label,"
|
||||
." COALESCE(version,1) AS version, fk_bericht_parent,"
|
||||
." status, final_pdf_path, fk_user_creat, fk_user_modif, datec, tms, note"
|
||||
." FROM ".$this->db->prefix()."bericht WHERE rowid = ".((int) $id);
|
||||
$res = $this->db->query($sql);
|
||||
|
|
@ -116,6 +121,8 @@ class Bericht extends CommonObject
|
|||
$this->page_orientation= $obj->page_orientation ?: 'P';
|
||||
$this->is_template = (int) $obj->is_template;
|
||||
$this->template_label = $obj->template_label;
|
||||
$this->version = (int) $obj->version;
|
||||
$this->fk_bericht_parent = $obj->fk_bericht_parent ? (int) $obj->fk_bericht_parent : null;
|
||||
$this->status = (int) $obj->status;
|
||||
$this->final_pdf_path = $obj->final_pdf_path;
|
||||
$this->fk_user_creat = $obj->fk_user_creat;
|
||||
|
|
@ -303,6 +310,55 @@ class Bericht extends CommonObject
|
|||
return (int) $b->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dupliziert einen Bericht als neue Version (v+1). Seiten werden kopiert.
|
||||
* Der alte Bericht bleibt unverändert.
|
||||
*
|
||||
* @return int|false Neue Bericht-ID
|
||||
*/
|
||||
public function duplicateAsNewVersion(User $user)
|
||||
{
|
||||
$new = new Bericht($this->db);
|
||||
$new->element_type = $this->element_type;
|
||||
$new->fk_element = $this->fk_element;
|
||||
$new->auftragsnummer = $this->auftragsnummer;
|
||||
$new->template_odt = $this->template_odt;
|
||||
$new->page_format = $this->page_format;
|
||||
$new->page_orientation = $this->page_orientation;
|
||||
$new->note = $this->note;
|
||||
// Parent der Versionskette = ursprünglicher Bericht oder dessen Parent
|
||||
$new->fk_bericht_parent = $this->fk_bericht_parent ?: $this->id;
|
||||
// Höchste bisherige Version in dieser Kette finden
|
||||
$chain_id = $new->fk_bericht_parent;
|
||||
$res = $this->db->query("SELECT MAX(COALESCE(version,1)) AS v FROM ".$this->db->prefix()."bericht"
|
||||
." WHERE rowid = ".((int) $chain_id)." OR fk_bericht_parent = ".((int) $chain_id));
|
||||
$max_v = 1;
|
||||
if ($res && ($r = $this->db->fetch_object($res))) $max_v = (int) $r->v;
|
||||
$new->version = $max_v + 1;
|
||||
$new->titel = ($this->titel ? preg_replace('/\s*\(v\d+\)$/', '', $this->titel) : 'Bericht').' (v'.$new->version.')';
|
||||
if ($new->create($user) <= 0) return false;
|
||||
|
||||
// Seiten kopieren
|
||||
$src_pages = BerichtPage::fetchAllForBericht($this->db, $this->id);
|
||||
foreach ($src_pages as $order => $p) {
|
||||
$np = new BerichtPage($this->db);
|
||||
$np->fk_bericht = $new->id;
|
||||
$np->page_order = $order + 1;
|
||||
$np->source_type = $p->source_type;
|
||||
$np->source_path = $p->source_path;
|
||||
$np->source_page = $p->source_page;
|
||||
$np->note = $p->note;
|
||||
$np->title = $p->title;
|
||||
$np->rotation = $p->rotation;
|
||||
$np->fabric_json = $p->fabric_json;
|
||||
$np->layout = $p->layout;
|
||||
$np->image_scale = $p->image_scale;
|
||||
$np->image_align = $p->image_align;
|
||||
$np->create();
|
||||
}
|
||||
return (int) $new->id;
|
||||
}
|
||||
|
||||
public function getLibStatut($mode = 0)
|
||||
{
|
||||
global $langs;
|
||||
|
|
@ -339,6 +395,7 @@ class BerichtPage
|
|||
public $rotation;
|
||||
public $fabric_json;
|
||||
public $note;
|
||||
public $title; // Optional Titel/Zwischentitel der Seite
|
||||
public $layout = 'single';
|
||||
public $image_scale = 1.0;
|
||||
public $image_align = 'fit';
|
||||
|
|
@ -395,11 +452,13 @@ class BerichtPage
|
|||
public static function slotCountForLayout($layout)
|
||||
{
|
||||
return array(
|
||||
'single' => 1,
|
||||
'grid_2' => 2,
|
||||
'grid_2v' => 2,
|
||||
'grid_4' => 4,
|
||||
'grid_6' => 6,
|
||||
'single' => 1,
|
||||
'grid_2' => 2,
|
||||
'grid_2v' => 2,
|
||||
'grid_4' => 4,
|
||||
'grid_6' => 6,
|
||||
'before_after' => 2,
|
||||
'title_only' => 0,
|
||||
)[$layout] ?? 1;
|
||||
}
|
||||
|
||||
|
|
@ -456,16 +515,17 @@ class BerichtPage
|
|||
public function create()
|
||||
{
|
||||
$sql = "INSERT INTO ".$this->db->prefix()."bericht_page ("
|
||||
."fk_bericht, page_order, source_type, source_path, source_page, rotation, fabric_json, note, layout, image_scale, image_align"
|
||||
."fk_bericht, page_order, source_type, source_path, source_page, rotation, fabric_json, note, title, layout, image_scale, image_align"
|
||||
.") VALUES ("
|
||||
.((int) $this->fk_bericht).","
|
||||
.((int) $this->page_order).","
|
||||
."'".$this->db->escape($this->source_type)."',"
|
||||
."'".$this->db->escape($this->source_path)."',"
|
||||
."'".$this->db->escape($this->source_path ?: '')."',"
|
||||
.($this->source_page !== null ? (int) $this->source_page : "NULL").","
|
||||
.((int) ($this->rotation ?? 0)).","
|
||||
.($this->fabric_json !== null ? "'".$this->db->escape($this->fabric_json)."'" : "NULL").","
|
||||
.($this->note ? "'".$this->db->escape($this->note)."'" : "NULL").","
|
||||
.($this->title ? "'".$this->db->escape($this->title)."'" : "NULL").","
|
||||
."'".$this->db->escape($this->layout ?: 'single')."',"
|
||||
.((float) ($this->image_scale ?: 1.0)).","
|
||||
."'".$this->db->escape($this->image_align ?: 'fit')."'"
|
||||
|
|
@ -498,7 +558,7 @@ class BerichtPage
|
|||
public static function fetchAllForBericht(DoliDB $db, $fk_bericht)
|
||||
{
|
||||
$list = array();
|
||||
$sql = "SELECT rowid, fk_bericht, page_order, source_type, source_path, source_page, rotation, fabric_json, note,"
|
||||
$sql = "SELECT rowid, fk_bericht, page_order, source_type, source_path, source_page, rotation, fabric_json, note, title,"
|
||||
." COALESCE(layout, 'single') AS layout,"
|
||||
." COALESCE(image_scale, 1.0) AS image_scale,"
|
||||
." COALESCE(image_align, 'fit') AS image_align"
|
||||
|
|
@ -518,6 +578,7 @@ class BerichtPage
|
|||
$p->rotation = (int) $obj->rotation;
|
||||
$p->fabric_json = $obj->fabric_json;
|
||||
$p->note = $obj->note;
|
||||
$p->title = $obj->title;
|
||||
$p->layout = $obj->layout;
|
||||
$p->image_scale = (float) $obj->image_scale;
|
||||
$p->image_align = $obj->image_align;
|
||||
|
|
|
|||
|
|
@ -155,6 +155,11 @@ class modBericht extends DolibarrModules
|
|||
// Phase 5.5: Bericht-Vorlagen
|
||||
"ALTER TABLE ".$this->db->prefix()."bericht ADD COLUMN is_template TINYINT(1) DEFAULT 0",
|
||||
"ALTER TABLE ".$this->db->prefix()."bericht ADD COLUMN template_label VARCHAR(255) DEFAULT NULL",
|
||||
// Phase C: Titel pro Seite (Zwischentitel)
|
||||
"ALTER TABLE ".$this->db->prefix()."bericht_page ADD COLUMN title VARCHAR(255) DEFAULT NULL",
|
||||
// Phase 5.3: Versionierung
|
||||
"ALTER TABLE ".$this->db->prefix()."bericht ADD COLUMN version INT DEFAULT 1",
|
||||
"ALTER TABLE ".$this->db->prefix()."bericht ADD COLUMN fk_bericht_parent INT DEFAULT NULL",
|
||||
);
|
||||
foreach ($migrations as $sql) {
|
||||
// Errors ignorieren — Spalten existieren ggf. schon
|
||||
|
|
|
|||
|
|
@ -219,7 +219,13 @@
|
|||
.page-thumb:hover { border-color: var(--colortextlink, #888); }
|
||||
.page-thumb.active {
|
||||
border-color: var(--colortextlink, #337ab7);
|
||||
box-shadow: 0 0 0 2px rgba(51,122,183,0.4);
|
||||
box-shadow: 0 0 0 3px rgba(51,122,183,0.55), 0 2px 12px rgba(51,122,183,0.3);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
.page-thumb.active .page-thumb-label {
|
||||
background: var(--colorbackhmenu1, #337ab7);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* "Papier"-Look — heller Default, mit Papier-Schatten */
|
||||
|
|
|
|||
|
|
@ -309,6 +309,73 @@ function bericht_render_page_to_pdf($pdf, $page, $ori, $fmt, $fpdi_loaded)
|
|||
{
|
||||
$layout = $page->layout ?: 'single';
|
||||
|
||||
// Spezial-Fall: reine Titel-Seite (kein Bild, nur großer Titel zentriert)
|
||||
if ($layout === 'title_only' || ($layout === 'single' && empty($page->source_path) && !empty($page->title))) {
|
||||
$pdf->AddPage($ori, $fmt);
|
||||
$pageW = $pdf->getPageWidth();
|
||||
$pageH = $pdf->getPageHeight();
|
||||
$pdf->SetFont('helvetica', 'B', 32);
|
||||
$pdf->SetTextColor(40, 40, 40);
|
||||
$pdf->SetY($pageH / 2 - 20);
|
||||
$pdf->MultiCell(0, 20, $page->title, 0, 'C');
|
||||
if (!empty($page->note)) {
|
||||
$pdf->Ln(12);
|
||||
$pdf->SetFont('helvetica', '', 12);
|
||||
$pdf->MultiCell(0, 8, $page->note, 0, 'C');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/* ----- Vorher/Nachher-Layout ----- */
|
||||
if ($layout === 'before_after') {
|
||||
$imgs = $page->getImages();
|
||||
if (empty($imgs)) return;
|
||||
$pdf->AddPage($ori, $fmt);
|
||||
$pageW = $pdf->getPageWidth();
|
||||
$pageH = $pdf->getPageHeight();
|
||||
$margin = 10;
|
||||
$title_h = !empty($page->title) ? 12 : 0;
|
||||
if ($title_h) {
|
||||
$pdf->SetFont('helvetica', 'B', 16);
|
||||
$pdf->SetY($margin);
|
||||
$pdf->MultiCell(0, $title_h, $page->title, 0, 'C');
|
||||
}
|
||||
// Zwei Bilder nebeneinander mit VORHER/NACHHER Labels
|
||||
$label_h = 8;
|
||||
$gap = 6;
|
||||
$top = $margin + $title_h + 4;
|
||||
$usable_h = $pageH - $top - 10 - $label_h; // -10 für Notiz, -label_h für Beschriftung
|
||||
$w_each = ($pageW - 2 * $margin - $gap) / 2;
|
||||
|
||||
foreach (array(array('Vorher', 0), array('Nachher', 1)) as $slot_info) {
|
||||
list($label, $slot) = $slot_info;
|
||||
if (!isset($imgs[$slot])) continue;
|
||||
$full = bericht_resolve_data_path($imgs[$slot]['source_path']);
|
||||
if (!$full || !file_exists($full)) continue;
|
||||
list($iw, $ih) = @getimagesize($full);
|
||||
if (!$iw || !$ih) continue;
|
||||
|
||||
$x0 = $margin + $slot * ($w_each + $gap);
|
||||
// Label
|
||||
$pdf->SetFont('helvetica', 'B', 11);
|
||||
$pdf->SetXY($x0, $top);
|
||||
$pdf->Cell($w_each, $label_h, $label, 0, 0, 'C');
|
||||
// Bild darunter
|
||||
$ratio = min($w_each / $iw, $usable_h / $ih);
|
||||
$w = $iw * $ratio; $h = $ih * $ratio;
|
||||
$x = $x0 + ($w_each - $w) / 2;
|
||||
$y = $top + $label_h + ($usable_h - $h) / 2;
|
||||
$pdf->Image($full, $x, $y, $w, $h);
|
||||
}
|
||||
bericht_burn_annotations($pdf, $page->fabric_json, 0, 0, $pageW, $pageH);
|
||||
if (!empty($page->note)) {
|
||||
$pdf->SetY(-20);
|
||||
$pdf->SetFont('helvetica', 'I', 9);
|
||||
$pdf->MultiCell(0, 5, $page->note, 0, 'L');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/* ----- Multi-Image Grid ----- */
|
||||
if ($layout !== 'single') {
|
||||
$imgs = $page->getImages();
|
||||
|
|
@ -317,7 +384,16 @@ function bericht_render_page_to_pdf($pdf, $page, $ori, $fmt, $fpdi_loaded)
|
|||
$pdf->AddPage($ori, $fmt);
|
||||
$pageW = $pdf->getPageWidth();
|
||||
$pageH = $pdf->getPageHeight();
|
||||
$rects = BerichtPage::slotRects($layout, $pageW, $pageH);
|
||||
|
||||
// Titel oben
|
||||
$title_h = 0;
|
||||
if (!empty($page->title)) {
|
||||
$pdf->SetFont('helvetica', 'B', 16);
|
||||
$pdf->SetY(10);
|
||||
$pdf->MultiCell(0, 10, $page->title, 0, 'C');
|
||||
$title_h = 14;
|
||||
}
|
||||
$rects = BerichtPage::slotRects($layout, $pageW, $pageH - $title_h, 10 + $title_h);
|
||||
|
||||
foreach ($imgs as $img) {
|
||||
$slot = $img['slot'];
|
||||
|
|
|
|||
Loading…
Reference in a new issue