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.

'; + +if (empty($rows)) { + print '
Noch keine Berichte für diesen Kunden vorhanden.
'; +} else { + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + + 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) + ? 'Final' + : 'Entwurf'; + + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + } + print '
'.$langs->trans("Ref").''.$langs->trans("BerichtTitle").'Quelle'.$langs->trans("BerichtCreatedAt").''.$langs->trans("BerichtStatus").''.$langs->trans("Action").'
'.dol_escape_htmltag($r->bref).''.dol_escape_htmltag($r->titel).''.$quelle_label.' '.dol_escape_htmltag($r->parent_ref).''.dol_print_date($db->jdate($r->datec), 'dayhour').''.$status_html.''; + print 'Öffnen '; + print 'Zur Quelle'; + print '
'; +} + +print '
'; + +llxFooter(); +$db->close(); diff --git a/class/bericht.class.php b/class/bericht.class.php index 1925d4c..e6aac2f 100644 --- a/class/bericht.class.php +++ b/class/bericht.class.php @@ -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")."," diff --git a/core/modules/modBericht.class.php b/core/modules/modBericht.class.php index c334619..b01abea 100644 --- a/core/modules/modBericht.class.php +++ b/core/modules/modBericht.class.php @@ -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); diff --git a/js/editor.js b/js/editor.js index a6737ac..4b4e61e 100644 --- a/js/editor.js +++ b/js/editor.js @@ -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); }); diff --git a/sql/llx_bericht.sql b/sql/llx_bericht.sql index 51ae041..530019c 100644 --- a/sql/llx_bericht.sql +++ b/sql/llx_bericht.sql @@ -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, diff --git a/sql/llx_bericht_page_image.key.sql b/sql/llx_bericht_page_image.key.sql new file mode 100644 index 0000000..e95c5a6 --- /dev/null +++ b/sql/llx_bericht_page_image.key.sql @@ -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; diff --git a/sql/llx_bericht_page_image.sql b/sql/llx_bericht_page_image.sql new file mode 100644 index 0000000..99cea02 --- /dev/null +++ b/sql/llx_bericht_page_image.sql @@ -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;