feat: Phase 1.3 + 1.7 + Schema 1.4/1.5 — Format/Orient + Kunden-Tab
All checks were successful
Deploy bericht / deploy (push) Successful in 1s

Phase 1.3 Seitenformat A4/A3/A5/Letter + Hoch/Quer:
- Neue Spalten page_format, page_orientation in llx_bericht
- Bericht-Meta zeigt Format + Orientation Selects
- Auto-Save via neuem ajax/save_meta.php
- generate_pdf + preview_pdf nutzen die gewählten Werte
- Bilder werden dynamisch via getPageWidth/getPageHeight skaliert
  (statt hardcoded 210x297 für A4)

Phase 1.4 + 1.5 Schema-Vorbereitung:
- Neue Tabelle llx_bericht_page_image für Multi-Image-Seiten
- Spalten layout, image_scale, image_align in llx_bericht_page
- DB-Migrationen im init() für bestehende Installationen
  (ALTER TABLE mit Error-Suppress)
- Grid-Rendering im Editor/PDF folgt im nächsten Commit
  (siehe CLAUDE.md TODO)

Phase 1.7 Tab "Berichte" auf Kundenkarte:
- Neue Konstante BERICHT_TAB_ON_THIRDPARTY (default 1)
- Tab-Definition in modBericht für 'thirdparty' Element
- Neue Datei bericht_thirdparty.php
- UNION-SQL über bericht JOIN commande/facture/propal mit fk_soc
- Read-only flache Tabelle sortiert nach Datum
- Pro Bericht: Quelle (Symbol + Ref-Link), Status, Öffnen/Zur Quelle

Version-Bump 1.0.0 → 1.1.0, ChangeLog ergänzt.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
[deploy]
This commit is contained in:
Eduard Wisch 2026-04-08 22:20:09 +02:00
parent a7bf3929a4
commit 0fbfb1bf27
13 changed files with 314 additions and 22 deletions

View file

@ -68,14 +68,21 @@ Speichert: color, stroke, fontFamily, fontSize, bold, italic, zoom
- Generate-PDF mit FPDI + Annotationen einbrennen
- Mobile-Upload-Idee dokumentiert (Phase 2)
### 🔄 Phase 1 Features (in Arbeit)
- [ ] **1.6 Verknüpfte Sicht Auftrag→Rechnung** — Übersicht zeigt zwei Sektionen, Übernahme-Button erzeugt llx_element_element. *(in Arbeit)*
- [ ] **1.1 Live-PDF-Vorschau** — neuer Endpoint `ajax/preview_pdf.php` (wie generate_pdf, aber ohne ECM-Insert + status), JS-Modal mit PDF.js
- [ ] **1.2 Anhänge löschen** — neuer Endpoint `ajax/delete_attachment.php`, 🗑️-Icon neben Checkbox in Anhänge-Liste, Path-Whitelist, ECM-Cleanup
- [ ] **1.3 Seitengröße A4/A3/Letter + Hoch/Quer** — neue Spalten in `llx_bericht` (format, orientation), pro Seite überschreibbar in `llx_bericht_page` (format_override)
- [ ] **1.4 Mehrere Bilder pro Seite** — Layout-Picker (1/2/4/6 Grid), neue Spalte `layout` in `llx_bericht_page`, n:m-Bilder evtl. neue Tabelle
- [ ] **1.5 Bildgröße pro Seite** — Spalten image_scale, image_align, image_x, image_y in llx_bericht_page
- [ ] **1.7 Kunden-Tab** — Tab "Berichte" auf thirdparty, read-only, flache Tabelle sortiert nach Datum, Konstante BERICHT_TAB_ON_THIRDPARTY (default 0)
### Phase 1 Features
- [x] **1.6 Verknüpfte Sicht Auftrag→Rechnung**
- [x] **1.1 Live-PDF-Vorschau**
- [x] **1.2 Anhänge löschen**
- [x] **1.3 Seitengröße A4/A3/Letter + Hoch/Quer** ✅ — global pro Bericht (nicht pro Seite override)
- [/] **1.4 Mehrere Bilder pro Seite** — DB + Klassen-Skelett da, Grid-Rendering im PDF + Editor noch offen (siehe TODO unten)
- [/] **1.5 Bildgröße pro Seite** — image_scale/image_align Spalten da, UI noch offen
- [ ] **1.7 Kunden-Tab** — Tab "Berichte" auf thirdparty
### TODO Phase 1.4 + 1.5 (Folge-Commit)
- BerichtPage::getImages() liest llx_bericht_page_image
- Editor: Layout-Dropdown pro Seite (1/2/4/6 Slots)
- Drag&Drop von Anhängen in Slots oder "Als Grid hinzufügen"-Button
- generate_pdf/preview_pdf: Grid-Rendering (calculate slot rects)
- Bei `single`-Layout: image_scale (1.0/0.7/0.5) + image_align (fit/center/topleft/topright)
---

View file

@ -1,5 +1,46 @@
# Changelog
## 1.1.0 — 2026-04-08
### Phase 1 Bericht-Modul Erweiterungen
**1.6 Verknüpfte Sicht Auftrag↔Rechnung**
- Bericht-Übersicht zeigt drei Sektionen: direkt zugeordnet, zusätzlich verknüpft, aus verknüpften Aufträgen
- "→ Übernehmen"-Button erstellt llx_element_element-Eintrag (n:m-Verknüpfung)
- "Lösen"-Button entfernt Verknüpfung
- Beim Finalisieren landet das PDF auch unter den verknüpften Elementen im ECM
**1.1 Live-PDF-Vorschau**
- 👁️ Vorschau-Button im Editor → Modal mit eingebettetem PDF
- Neuer Endpoint `ajax/preview_pdf.php` (kein ECM-Insert, kein Status-Wechsel)
- ESC oder Klick auf Backdrop schließt das Modal
**1.2 Anhänge löschen**
- 🗑️ Icon neben jedem Anhang in der linken Spalte
- Confirm-Dialog mit Auftrags-/Rechnungs-Referenz
- Path-Whitelist (nur facture/, commande/, propal/), Thumbs + ECM-Eintrag werden mitgelöscht
**1.3 Seitengröße A4/A3/A5/Letter + Hoch/Quer**
- Format und Orientation in der Bericht-Meta wählbar
- Auto-Save bei Änderung
- Bilder werden dynamisch auf die Seitengröße skaliert
**1.4 + 1.5 Mehrere Bilder pro Seite (DB-Schema)**
- Neue Tabelle `llx_bericht_page_image` für Multi-Image-Seiten
- Spalten `layout`, `image_scale`, `image_align` in `llx_bericht_page`
- Grid-Rendering im Editor + PDF folgt im nächsten Commit
**1.7 Tab „Berichte" auf Kundenkarte**
- Read-only Übersicht aller Berichte des Kunden
- Flache Tabelle sortiert nach Datum
- Springt zum Bericht oder zur Quelle (Auftrag/Rechnung/Angebot)
- Konstante `BERICHT_TAB_ON_THIRDPARTY` zum Aktivieren
### Sonstiges
- DB-Migrationen im `init()` für bestehende Installationen (ALTER TABLE mit Error-Suppress)
- `bericht_burn_annotations` und `bericht_render_cover_internal` in `lib/bericht.lib.php` zentralisiert (gemeinsam von generate_pdf + preview_pdf genutzt)
- Modal-CSS für Vorschau im Dolibarr Dark-Theme
## 1.0.0 — 2026-04-08
Initiales Release.

View file

@ -41,10 +41,12 @@ foreach (array(
// FPDI ist optional — wenn fehlt, können wir keine bestehenden PDFs einbetten,
// aber Bilder + Annotationen funktionieren weiterhin.
$ori = in_array($bericht->page_orientation, array('P','L'), true) ? $bericht->page_orientation : 'P';
$fmt = in_array($bericht->page_format, array('A4','A3','A5','Letter'), true) ? $bericht->page_format : 'A4';
if ($fpdi_loaded) {
$pdf = new \setasign\Fpdi\Tcpdf\Fpdi('P', 'mm', 'A4', true, 'UTF-8', false);
$pdf = new \setasign\Fpdi\Tcpdf\Fpdi($ori, 'mm', $fmt, true, 'UTF-8', false);
} else {
$pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false);
$pdf = new TCPDF($ori, 'mm', $fmt, true, 'UTF-8', false);
}
$pdf->SetCreator('Dolibarr Bericht-Modul');
$pdf->SetAuthor($user->getFullName($langs));
@ -80,16 +82,19 @@ foreach ($pages as $page) {
if (!$full || !file_exists($full)) continue;
if ($page->source_type === 'image' || preg_match('/\.(png|jpe?g)$/i', $full)) {
// Bild als A4-Seite
$pdf->AddPage('P', 'A4');
// Bild als Seite im konfigurierten Format/Orientation
$pdf->AddPage($ori, $fmt);
list($iw, $ih) = @getimagesize($full);
if ($iw && $ih) {
$maxW = 190; $maxH = 277;
$pageW = $pdf->getPageWidth();
$pageH = $pdf->getPageHeight();
$margin = 10;
$maxW = $pageW - 2 * $margin;
$maxH = $pageH - 2 * $margin - 10; // 10mm für Notiz unten
$ratio = min($maxW / $iw, $maxH / $ih);
$w = $iw * $ratio; $h = $ih * $ratio;
$x = (210 - $w) / 2; $y = 10;
$x = ($pageW - $w) / 2; $y = $margin;
$pdf->Image($full, $x, $y, $w, $h);
// Annotationen drauf
bericht_burn_annotations($pdf, $page->fabric_json, $x, $y, $w, $h);
}
} elseif ($fpdi_loaded && preg_match('/\.pdf$/i', $full)) {

View file

@ -51,10 +51,12 @@ foreach (array(
DOL_DOCUMENT_ROOT.'/includes/fpdi/src/Tcpdf/Fpdi.php',
) as $p) { if (file_exists($p)) { require_once $p; $fpdi_loaded = true; break; } }
$ori = in_array($bericht->page_orientation, array('P','L'), true) ? $bericht->page_orientation : 'P';
$fmt = in_array($bericht->page_format, array('A4','A3','A5','Letter'), true) ? $bericht->page_format : 'A4';
if ($fpdi_loaded) {
$pdf = new \setasign\Fpdi\Tcpdf\Fpdi('P', 'mm', 'A4', true, 'UTF-8', false);
$pdf = new \setasign\Fpdi\Tcpdf\Fpdi($ori, 'mm', $fmt, true, 'UTF-8', false);
} else {
$pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false);
$pdf = new TCPDF($ori, 'mm', $fmt, true, 'UTF-8', false);
}
$pdf->SetCreator('Dolibarr Bericht-Modul (Vorschau)');
$pdf->SetAuthor($user->getFullName($langs));
@ -90,13 +92,17 @@ foreach ($pages as $page) {
if (!$full || !file_exists($full)) continue;
if ($page->source_type === 'image' || preg_match('/\.(png|jpe?g)$/i', $full)) {
$pdf->AddPage('P', 'A4');
$pdf->AddPage($ori, $fmt);
list($iw, $ih) = @getimagesize($full);
if ($iw && $ih) {
$maxW = 190; $maxH = 277;
$pageW = $pdf->getPageWidth();
$pageH = $pdf->getPageHeight();
$margin = 10;
$maxW = $pageW - 2 * $margin;
$maxH = $pageH - 2 * $margin - 10;
$ratio = min($maxW / $iw, $maxH / $ih);
$w = $iw * $ratio; $h = $ih * $ratio;
$x = (210 - $w) / 2; $y = 10;
$x = ($pageW - $w) / 2; $y = $margin;
$pdf->Image($full, $x, $y, $w, $h);
if (function_exists('bericht_burn_annotations')) {
bericht_burn_annotations($pdf, $page->fabric_json, $x, $y, $w, $h);

20
ajax/save_meta.php Normal file
View file

@ -0,0 +1,20 @@
<?php
/* Speichert Meta-Felder eines Berichts (Titel, Template, Format, Orientation).
* POST: berichtid, titel, template_odt, page_format, page_orientation, token
*/
require_once __DIR__.'/_inc.php';
global $db, $user;
if (!$user->hasRight('bericht', 'write')) bericht_ajax_fail('Permission denied', 403);
$berichtid = (int) ($_POST['berichtid'] ?? 0);
$bericht = new Bericht($db);
if ($bericht->fetch($berichtid) <= 0) bericht_ajax_fail('Bericht nicht gefunden', 404);
if (isset($_POST['titel'])) $bericht->titel = (string) $_POST['titel'];
if (isset($_POST['template_odt'])) $bericht->template_odt = (string) $_POST['template_odt'];
if (isset($_POST['page_format'])) $bericht->page_format = in_array($_POST['page_format'], array('A4','A3','A5','Letter'), true) ? $_POST['page_format'] : 'A4';
if (isset($_POST['page_orientation'])) $bericht->page_orientation = in_array($_POST['page_orientation'], array('P','L'), true) ? $_POST['page_orientation'] : 'P';
if ($bericht->update($user) > 0) bericht_ajax_ok();
bericht_ajax_fail($bericht->error ?: 'Update fehlgeschlagen');

View file

@ -261,6 +261,7 @@ if (!$bericht) {
'generate_pdf' => dol_buildpath('/bericht/ajax/generate_pdf.php', 1),
'preview_pdf' => dol_buildpath('/bericht/ajax/preview_pdf.php', 1),
'delete_attachment'=> dol_buildpath('/bericht/ajax/delete_attachment.php', 1),
'save_meta' => dol_buildpath('/bericht/ajax/save_meta.php', 1),
),
'lang' => array(
'undo' => $langs->trans("BerichtUndo"),
@ -296,6 +297,24 @@ if (!$bericht) {
print '<option value="'.dol_escape_htmltag($tpl).'"'.$sel.'>'.dol_escape_htmltag($tpl).'</option>';
}
print '</select></td>';
print '</tr><tr>';
// Format und Orientation
print '<td>Format</td><td>';
print '<select id="meta-format" name="page_format" title="Seitenformat des PDFs">';
foreach (array('A4', 'A3', 'A5', 'Letter') as $fmt) {
$sel = ($fmt === $bericht->page_format) ? ' selected' : '';
print '<option value="'.$fmt.'"'.$sel.'>'.$fmt.'</option>';
}
print '</select>';
print ' &nbsp; ';
print '<select id="meta-orientation" name="page_orientation" title="Hoch- oder Querformat">';
foreach (array('P' => 'Hochformat', 'L' => 'Querformat') as $k => $v) {
$sel = ($k === $bericht->page_orientation) ? ' selected' : '';
print '<option value="'.$k.'"'.$sel.'>'.$v.'</option>';
}
print '</select>';
print '</td>';
print '<td colspan="2"><span class="opacitymedium small">Wird beim Finalisieren angewendet</span></td>';
print '</tr></table>';
print '</form>';
print '</div>';

128
bericht_thirdparty.php Normal file
View file

@ -0,0 +1,128 @@
<?php
/* Read-only Übersicht aller Berichte eines Kunden.
* Listet ALLE Berichte zu den Aufträgen + Rechnungen + Angeboten dieses Kunden,
* sortiert nach Datum (neueste zuerst).
*
* NICHT zum Anlegen von Berichten. Hier wird kein Bericht direkt am Kunden gespeichert.
*
* GET: socid (thirdparty id)
*/
$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 DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
require_once DOL_DOCUMENT_ROOT.'/core/lib/company.lib.php';
require_once __DIR__.'/class/bericht.class.php';
if (!$user->hasRight('bericht', 'read')) accessforbidden();
if (!$user->hasRight('societe', 'lire')) accessforbidden();
$langs->loadLangs(array("bericht@bericht", "main", "companies"));
$socid = GETPOSTINT('socid');
if (!$socid) accessforbidden('socid fehlt');
$soc = new Societe($db);
if ($soc->fetch($socid) <= 0) accessforbidden('Kunde nicht gefunden');
// Berichte des Kunden ermitteln (ein UNION über alle drei Element-Typen)
$sql = "SELECT b.rowid, b.ref AS bref, b.titel, b.datec, b.status, b.element_type,"
." 'order' AS quelle, c.ref AS parent_ref, c.rowid AS parent_id"
." FROM ".$db->prefix()."bericht b"
." INNER JOIN ".$db->prefix()."commande c ON c.rowid = b.fk_element"
." WHERE b.element_type = 'order' AND c.fk_soc = ".((int) $socid)
." UNION "
."SELECT b.rowid, b.ref AS bref, b.titel, b.datec, b.status, b.element_type,"
." 'invoice' AS quelle, f.ref AS parent_ref, f.rowid AS parent_id"
." FROM ".$db->prefix()."bericht b"
." INNER JOIN ".$db->prefix()."facture f ON f.rowid = b.fk_element"
." WHERE b.element_type = 'invoice' AND f.fk_soc = ".((int) $socid)
." UNION "
."SELECT b.rowid, b.ref AS bref, b.titel, b.datec, b.status, b.element_type,"
." 'propal' AS quelle, p.ref AS parent_ref, p.rowid AS parent_id"
." FROM ".$db->prefix()."bericht b"
." INNER JOIN ".$db->prefix()."propal p ON p.rowid = b.fk_element"
." WHERE b.element_type = 'propal' AND p.fk_soc = ".((int) $socid)
." ORDER BY datec DESC";
$rows = array();
$resq = $db->query($sql);
if ($resq) {
while ($obj = $db->fetch_object($resq)) {
$rows[] = $obj;
}
}
llxHeader('', $langs->trans("Berichte").' — '.$soc->name, '', '', 0, 0, array(), array(), '', 'mod-bericht page-bericht-thirdparty');
$head = societe_prepare_head($soc);
print dol_get_fiche_head($head, 'bericht', $langs->trans("ThirdParty"), -1, 'company');
dol_banner_tab($soc, 'socid', '', ($user->socid ? 0 : 1), 'rowid', 'nom');
print dol_get_fiche_end();
print '<br>';
print '<div class="bericht-thirdparty-list">';
print '<h3>'.$langs->trans("Berichte").' ('.count($rows).')</h3>';
print '<p class="opacitymedium small">Read-only Übersicht aller Berichte aus Aufträgen, Rechnungen und Angeboten dieses Kunden. Bericht-Anlage erfolgt direkt am jeweiligen Auftrag oder der Rechnung.</p>';
if (empty($rows)) {
print '<div class="opacitymedium" style="padding:20px;">Noch keine Berichte für diesen Kunden vorhanden.</div>';
} else {
print '<table class="noborder centpercent">';
print '<tr class="liste_titre">';
print '<th>'.$langs->trans("Ref").'</th>';
print '<th>'.$langs->trans("BerichtTitle").'</th>';
print '<th>Quelle</th>';
print '<th>'.$langs->trans("BerichtCreatedAt").'</th>';
print '<th>'.$langs->trans("BerichtStatus").'</th>';
print '<th class="right">'.$langs->trans("Action").'</th>';
print '</tr>';
foreach ($rows as $r) {
$bericht_url = dol_buildpath('/bericht/bericht_card.php', 1).'?berichtid='.$r->rowid;
// Parent-URL berechnen
if ($r->quelle === 'order') {
$parent_url = DOL_URL_ROOT.'/commande/card.php?id='.$r->parent_id;
$quelle_label = '🛒 Auftrag';
} elseif ($r->quelle === 'invoice') {
$parent_url = DOL_URL_ROOT.'/compta/facture/card.php?id='.$r->parent_id;
$quelle_label = '📄 Rechnung';
} else {
$parent_url = DOL_URL_ROOT.'/comm/propal/card.php?id='.$r->parent_id;
$quelle_label = '📋 Angebot';
}
$status_html = ((int) $r->status === 1)
? '<span class="badge badge-status4">Final</span>'
: '<span class="badge badge-status0">Entwurf</span>';
print '<tr class="oddeven">';
print '<td><a href="'.$bericht_url.'">'.dol_escape_htmltag($r->bref).'</a></td>';
print '<td>'.dol_escape_htmltag($r->titel).'</td>';
print '<td>'.$quelle_label.' <a href="'.$parent_url.'">'.dol_escape_htmltag($r->parent_ref).'</a></td>';
print '<td>'.dol_print_date($db->jdate($r->datec), 'dayhour').'</td>';
print '<td>'.$status_html.'</td>';
print '<td class="right">';
print '<a href="'.$bericht_url.'" class="button-small">Öffnen</a> ';
print '<a href="'.$parent_url.'" class="button-small">Zur Quelle</a>';
print '</td>';
print '</tr>';
}
print '</table>';
}
print '</div>';
llxFooter();
$db->close();

View file

@ -24,6 +24,8 @@ class Bericht extends CommonObject
public $fk_element;
public $auftragsnummer;
public $template_odt;
public $page_format = 'A4'; // A4, A3, Letter
public $page_orientation = 'P'; // P=Portrait, L=Landscape
public $status; // 0 = Entwurf, 1 = Final
public $final_pdf_path;
public $fk_user_creat;
@ -54,7 +56,7 @@ class Bericht extends CommonObject
}
$sql = "INSERT INTO ".$this->db->prefix()."bericht ("
."entity, ref, titel, element_type, fk_element, auftragsnummer, template_odt, status, fk_user_creat, datec, note"
."entity, ref, titel, element_type, fk_element, auftragsnummer, template_odt, page_format, page_orientation, status, fk_user_creat, datec, note"
.") VALUES ("
.((int) $this->entity).","
."'".$this->db->escape($this->ref)."',"
@ -63,6 +65,8 @@ class Bericht extends CommonObject
.((int) $this->fk_element).","
.($this->auftragsnummer ? "'".$this->db->escape($this->auftragsnummer)."'" : "NULL").","
.($this->template_odt ? "'".$this->db->escape($this->template_odt)."'" : "NULL").","
."'".$this->db->escape($this->page_format)."',"
."'".$this->db->escape($this->page_orientation)."',"
.((int) $this->status).","
.((int) $this->fk_user_creat).","
."'".$this->db->idate($this->datec)."',"
@ -84,6 +88,7 @@ class Bericht extends CommonObject
public function fetch($id)
{
$sql = "SELECT rowid, entity, ref, titel, element_type, fk_element, auftragsnummer, template_odt,"
." page_format, page_orientation,"
." 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);
@ -103,6 +108,8 @@ class Bericht extends CommonObject
$this->fk_element = $obj->fk_element;
$this->auftragsnummer = $obj->auftragsnummer;
$this->template_odt = $obj->template_odt;
$this->page_format = $obj->page_format ?: 'A4';
$this->page_orientation= $obj->page_orientation ?: 'P';
$this->status = (int) $obj->status;
$this->final_pdf_path = $obj->final_pdf_path;
$this->fk_user_creat = $obj->fk_user_creat;
@ -119,6 +126,8 @@ class Bericht extends CommonObject
."titel = ".($this->titel ? "'".$this->db->escape($this->titel)."'" : "NULL").","
."auftragsnummer = ".($this->auftragsnummer ? "'".$this->db->escape($this->auftragsnummer)."'" : "NULL").","
."template_odt = ".($this->template_odt ? "'".$this->db->escape($this->template_odt)."'" : "NULL").","
."page_format = '".$this->db->escape($this->page_format ?: 'A4')."',"
."page_orientation = '".$this->db->escape($this->page_orientation ?: 'P')."',"
."status = ".((int) $this->status).","
."final_pdf_path = ".($this->final_pdf_path ? "'".$this->db->escape($this->final_pdf_path)."'" : "NULL").","
."note = ".($this->note ? "'".$this->db->escape($this->note)."'" : "NULL").","

View file

@ -27,7 +27,7 @@ class modBericht extends DolibarrModules
$this->editor_name = 'Alles Watt läuft';
$this->editor_url = '';
$this->version = '1.0.0';
$this->version = '1.1.0';
$this->const_name = 'MAIN_MODULE_'.strtoupper($this->name);
$this->picto = 'fa-file-pdf';
@ -74,6 +74,7 @@ class modBericht extends DolibarrModules
3 => array('BERICHT_TAB_ON_PROPAL', 'chaine', '1', 'Reiter Bericht auf Angeboten anzeigen', 0, 'current', 0),
4 => array('BERICHT_BURN_ANNOTATIONS', 'chaine', '1', 'Annotationen beim Export ins PDF einbrennen', 0, 'current', 0),
5 => array('BERICHT_LIBREOFFICE_BIN', 'chaine', '/usr/bin/libreoffice', 'Pfad zu LibreOffice für ODT→PDF Konvertierung', 0, 'current', 0),
6 => array('BERICHT_TAB_ON_THIRDPARTY', 'chaine', '1', 'Reiter Berichte auf Kundenkarten (read-only Übersicht)', 0, 'current', 0),
);
// Tabs werden über den Hook (actions_bericht.class.php → addMoreActionsButtons / completeTabsHead)
@ -83,6 +84,7 @@ class modBericht extends DolibarrModules
'invoice:+bericht:Bericht:bericht@bericht:$user->hasRight("bericht","read"):/custom/bericht/bericht_card.php?id=__ID__&element=invoice',
'order:+bericht:Bericht:bericht@bericht:$user->hasRight("bericht","read"):/custom/bericht/bericht_card.php?id=__ID__&element=order',
'propal:+bericht:Bericht:bericht@bericht:$user->hasRight("bericht","read"):/custom/bericht/bericht_card.php?id=__ID__&element=propal',
'thirdparty:+bericht:Berichte:bericht@bericht:$user->hasRight("bericht","read"):/custom/bericht/bericht_thirdparty.php?socid=__ID__',
);
$this->dictionaries = array();
@ -138,6 +140,22 @@ class modBericht extends DolibarrModules
return -1;
}
// Migrationen für bestehende Tabellen
$migrations = array(
// Phase 1.3: Seitenformat
"ALTER TABLE ".$this->db->prefix()."bericht ADD COLUMN page_format VARCHAR(8) DEFAULT 'A4'",
"ALTER TABLE ".$this->db->prefix()."bericht ADD COLUMN page_orientation VARCHAR(8) DEFAULT 'P'",
// Phase 1.4: Layout für mehrere Bilder pro Seite
"ALTER TABLE ".$this->db->prefix()."bericht_page ADD COLUMN layout VARCHAR(16) DEFAULT 'single'",
// Phase 1.5: Bildgröße/-position
"ALTER TABLE ".$this->db->prefix()."bericht_page ADD COLUMN image_scale FLOAT DEFAULT 1.0",
"ALTER TABLE ".$this->db->prefix()."bericht_page ADD COLUMN image_align VARCHAR(16) DEFAULT 'fit'",
);
foreach ($migrations as $sql) {
// Errors ignorieren — Spalten existieren ggf. schon
$this->db->query($sql, 1);
}
// Extrafields auf facture sicherstellen — vorhandene werden NICHT angefasst
require_once DOL_DOCUMENT_ROOT.'/core/class/extrafields.class.php';
$extrafields = new ExtraFields($this->db);

View file

@ -565,8 +565,33 @@
if (showMessage && data.success) toast('Seite gespeichert');
}
/* ---------- Meta-Felder Auto-Save ---------- */
async function saveMeta() {
const fd = new FormData();
fd.append('token', cfg.token);
fd.append('berichtid', cfg.berichtid);
const titelEl = document.querySelector('input[name="titel"]');
const tplEl = document.querySelector('select[name="template_odt"]');
const fmtEl = document.getElementById('meta-format');
const oriEl = document.getElementById('meta-orientation');
if (titelEl) fd.append('titel', titelEl.value);
if (tplEl) fd.append('template_odt', tplEl.value);
if (fmtEl) fd.append('page_format', fmtEl.value);
if (oriEl) fd.append('page_orientation', oriEl.value);
await fetch(cfg.urls.save_meta, { method: 'POST', body: fd });
}
function bindMetaAutoSave() {
['input[name="titel"]', 'select[name="template_odt"]', '#meta-format', '#meta-orientation'].forEach(sel => {
const el = document.querySelector(sel);
if (!el) return;
el.addEventListener('change', saveMeta);
});
}
function bindActions() {
bindMetaAutoSave();
document.getElementById('btn-save-draft').addEventListener('click', async () => {
await saveMeta();
await savePageAnnotations(true);
});

View file

@ -10,6 +10,8 @@ CREATE TABLE llx_bericht (
fk_element INTEGER NOT NULL, -- ID des Parent-Objekts
auftragsnummer VARCHAR(255) DEFAULT NULL,
template_odt VARCHAR(255) DEFAULT NULL, -- Dateiname aus templates/
page_format VARCHAR(8) DEFAULT 'A4', -- A4, A3, Letter
page_orientation VARCHAR(8) DEFAULT 'P', -- P=Portrait, L=Landscape
status INTEGER DEFAULT 0 NOT NULL, -- 0=Entwurf, 1=Final
final_pdf_path VARCHAR(512) DEFAULT NULL, -- Pfad relativ zu DOL_DATA_ROOT
fk_user_creat INTEGER NOT NULL,

View file

@ -0,0 +1,2 @@
ALTER TABLE llx_bericht_page_image ADD INDEX idx_bpi_page (fk_page, slot);
ALTER TABLE llx_bericht_page_image ADD CONSTRAINT fk_bpi_page FOREIGN KEY (fk_page) REFERENCES llx_bericht_page(rowid) ON DELETE CASCADE;

View file

@ -0,0 +1,10 @@
-- Mehrere Bilder pro Seite (Phase 1.4).
-- Wenn llx_bericht_page.layout != 'single' enthält die Seite ein Grid aus mehreren Bildern.
CREATE TABLE llx_bericht_page_image (
rowid INTEGER AUTO_INCREMENT PRIMARY KEY,
fk_page INTEGER NOT NULL,
slot INTEGER NOT NULL, -- 0-basierter Slot im Grid
source_path VARCHAR(512) NOT NULL,
rotation INTEGER DEFAULT 0,
tms TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL
) ENGINE=innodb;