diff --git a/CLAUDE.md b/CLAUDE.md
index f63d8ef..ff6586c 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -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)
---
diff --git a/ChangeLog.md b/ChangeLog.md
index 2b55d73..de7d018 100644
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -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.
diff --git a/ajax/generate_pdf.php b/ajax/generate_pdf.php
index aa9d4fd..0ce166c 100644
--- a/ajax/generate_pdf.php
+++ b/ajax/generate_pdf.php
@@ -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)) {
diff --git a/ajax/preview_pdf.php b/ajax/preview_pdf.php
index 9187c4d..7eb9cc6 100644
--- a/ajax/preview_pdf.php
+++ b/ajax/preview_pdf.php
@@ -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);
diff --git a/ajax/save_meta.php b/ajax/save_meta.php
new file mode 100644
index 0000000..566f397
--- /dev/null
+++ b/ajax/save_meta.php
@@ -0,0 +1,20 @@
+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');
diff --git a/bericht_card.php b/bericht_card.php
index e0c076b..eb7eeeb 100644
--- a/bericht_card.php
+++ b/bericht_card.php
@@ -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 '';
}
print '';
+ print '
';
+ // Format und Orientation
+ print '
Format
';
+ print '';
+ print ' ';
+ print '';
+ print '
';
+ print '
Wird beim Finalisieren angewendet
';
print '
';
print '';
print '';
diff --git a/bericht_thirdparty.php b/bericht_thirdparty.php
new file mode 100644
index 0000000..0c9260f
--- /dev/null
+++ b/bericht_thirdparty.php
@@ -0,0 +1,128 @@
+ 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 ' ';
+
+print '
';
+print '
'.$langs->trans("Berichte").' ('.count($rows).')
';
+print '
Read-only Übersicht aller Berichte aus Aufträgen, Rechnungen und Angeboten dieses Kunden. Bericht-Anlage erfolgt direkt am jeweiligen Auftrag oder der Rechnung.