bericht/core/modules/modBericht.class.php
Eduard Wisch 195942a2f9
All checks were successful
Deploy bericht / deploy (push) Successful in 2s
feat: Phase 6 — Client-WYSIWYG via Composite-PNG + Text-BG + Dark-Input-Fix
Paradigmen-Wechsel: Editor rendert bei jedem Save sein Fabric-Canvas
als PNG und lädt es hoch. PDF nutzt dieses PNG 1:1 statt die Shapes
serverseitig nachzuzeichnen.

Damit ist garantiert: was du im Editor siehst, ist EXAKT das was im PDF
landet. Alle Pfeil/Text/Shape-Rendering-Bugs zwischen Fabric-JSON und
PHP-Nachzeichnung sind Geschichte.

Kernänderungen:

1. DB: Neue Spalte bericht_page.composite_path (Migration im init())
2. ajax/save_annotations.php: nimmt multipart file 'composite' entgegen,
   speichert es unter bericht/work/<fkb>/composite_<pid>.png
3. lib/bericht.lib.php: bericht_render_page_to_pdf prüft composite_path
   zuerst — wenn vorhanden, wird eine Seite mit genau diesem PNG als
   volles Bild gerendert, fertig. Fallback auf alte Logik bei alten
   Berichten ohne Composite.
4. editor.js renderImage: Quellbild wird NICHT mehr auf pdfCanvas
   gezeichnet, sondern als fabric.Image ins Fabric-Canvas geladen —
   ZIEHBAR, SKALIERBAR, ROTIERBAR wie jedes andere Objekt.
   Mehrere Bilder auf einer Seite kein Problem mehr.
5. editor.js savePageAnnotations: nach Shape-State wird toDataURL
   mit multiplier:2 aufgerufen, PNG-Blob hochgeladen zusammen mit
   fabric_json (für spätere Edits) und note.
6. editor.js loadPage: wenn fabric_json existiert, wird dieses
   clientseitig wieder eingeladen (inkl. eingebettete Bilder) — das
   Quell-Bild wird nicht mehr neu aus der Quelle geholt. Bei leerer
   Seite läuft der alte Render-Flow.

Phase 6 Bonus — Text mit Hintergrund:
- Neuer color-picker 'BG:' in der Toolbar + 'Ø'-Button (kein BG)
- Fabric IText bekommt textBackgroundColor + padding:6
- Bei selektiertem Text-Objekt wird BG live angewendet
- Dataset-Flag 'active' toggelt zwischen ein/aus

Dark-Input-Fix:
- Textarea in .bericht-page-note nutzte --inputbackgroundcolor
  (existiert in awl-dark nicht → Fallback #fff = weiße Fläche mit
  schwarzer Schrift auf Dark-Theme)
- Jetzt: --colorbackbody + --colortext + --colorboxbordertitle1
- Generischer Input-Style für alle Text-Eingaben in .bericht-editor

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
[deploy]
2026-04-09 13:26:57 +02:00

230 lines
11 KiB
PHP

<?php
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
* GPL v3+ — siehe COPYING
*/
/**
* \defgroup bericht Module Bericht
* \brief Arbeitsberichte aus Anhängen erstellen, im Browser annotieren und an Rechnungen anhängen.
* \file htdocs/bericht/core/modules/modBericht.class.php
*/
include_once DOL_DOCUMENT_ROOT.'/core/modules/DolibarrModules.class.php';
class modBericht extends DolibarrModules
{
public function __construct($db)
{
global $conf, $langs;
$this->db = $db;
$this->numero = 500033; // Frei (500021 kollidiert mit BankImport, siehe llx_rights_def)
$this->rights_class = 'bericht';
$this->family = "other";
$this->module_position = '90';
$this->name = preg_replace('/^mod/i', '', get_class($this));
$this->description = "Arbeitsberichte aus Rechnungs-Anhängen erstellen, im Browser annotieren und als PDF an die Rechnung anhängen.";
$this->descriptionlong = "Fügt Rechnungen, Aufträgen und Angeboten einen Reiter 'Bericht' hinzu. Anhänge auswählen, im Browser mit Pfeilen/Kreisen/Rechtecken/Text annotieren, Seiten verwalten, Deckblatt aus ODT-Vorlage einfügen und als PDF unter Verknüpfte Dokumente speichern.";
$this->editor_name = 'Alles Watt läuft';
$this->editor_url = '';
$this->version = '1.1.0';
$this->const_name = 'MAIN_MODULE_'.strtoupper($this->name);
$this->picto = 'fa-file-pdf';
$this->module_parts = array(
'triggers' => 0,
'login' => 0,
'substitutions' => 0,
'menus' => 0,
'tpl' => 0,
'barcode' => 0,
'models' => 0,
'printing' => 0,
'theme' => 0,
'css' => array('/bericht/css/bericht.css'),
'js' => array(),
'hooks' => array(),
'moduleforexternal' => 0,
);
// Datenverzeichnisse
$this->dirs = array(
"/bericht/temp",
"/bericht/templates",
"/bericht/work",
);
// Konfigurationsseite im Admin-Bereich
$this->config_page_url = array("setup.php@bericht");
$this->hidden = false;
$this->depends = array();
$this->requiredby = array();
$this->conflictwith = array();
$this->langfiles = array("bericht@bericht");
$this->phpmin = array(7, 4);
$this->need_dolibarr_version = array(19, 0);
$this->need_javascript_ajax = 1;
// Konstanten beim Aktivieren anlegen
$this->const = array(
0 => array('BERICHT_DEFAULT_TEMPLATE', 'chaine', '', 'Standard ODT-Template für Deckblatt', 0, 'current', 0),
1 => array('BERICHT_TAB_ON_INVOICE', 'chaine', '1', 'Reiter Bericht auf Rechnungen anzeigen', 0, 'current', 0),
2 => array('BERICHT_TAB_ON_ORDER', 'chaine', '1', 'Reiter Bericht auf Aufträgen anzeigen', 0, 'current', 0),
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)
// dynamisch hinzugefügt, weil wir die Sichtbarkeit pro Element-Typ über Konstanten steuern wollen.
// Statisch geht aber auch — sicherer und einfacher:
$this->tabs = array(
'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();
$this->boxes = array();
// Kein Cronjob — Cleanup expired Upload-Tokens passiert on-demand
// beim Anlegen eines neuen Tokens (siehe BerichtUploadToken::create()).
$this->cronjobs = array();
// Rechte — wie Stundenzettel: [4]=perms, [5]=subperms (leer)
$this->rights = array();
$r = 0;
$this->rights[$r][0] = $this->numero + $r;
$this->rights[$r][1] = 'Berichte lesen';
$this->rights[$r][3] = 1; // Standard aktiviert
$this->rights[$r][4] = 'read';
$this->rights[$r][5] = '';
$r++;
$this->rights[$r][0] = $this->numero + $r;
$this->rights[$r][1] = 'Berichte erstellen und bearbeiten';
$this->rights[$r][3] = 1;
$this->rights[$r][4] = 'write';
$this->rights[$r][5] = '';
$r++;
$this->rights[$r][0] = $this->numero + $r;
$this->rights[$r][1] = 'Berichte löschen';
$this->rights[$r][3] = 1;
$this->rights[$r][4] = 'delete';
$this->rights[$r][5] = '';
$r++;
$this->rights[$r][0] = $this->numero + $r;
$this->rights[$r][1] = 'Modul Bericht administrieren (Templates verwalten)';
$this->rights[$r][3] = 0;
$this->rights[$r][4] = 'admin';
$this->rights[$r][5] = '';
$r++;
$this->menu = array();
}
/**
* Beim Aktivieren ausgeführt: SQL laden, Verzeichnisse anlegen,
* vorhandene Extrafields auf llx_facture_extrafields prüfen und ggf. anlegen.
*/
public function init($options = '')
{
global $conf, $langs;
// SQL-Tabellen anlegen
$result = $this->_load_tables('/bericht/sql/');
if ($result < 0) {
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'",
// 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",
// Phase 6: Composite-PNG (Client-WYSIWYG). Wenn gesetzt, wird die Seite
// komplett aus diesem einen PNG gerendert statt aus source_path + fabric_json.
// Der Editor rendert sein Fabric-Canvas bei jedem Save zu einem PNG und
// lädt es hoch — damit ist PDF-Output identisch mit Editor-Anzeige.
"ALTER TABLE ".$this->db->prefix()."bericht_page ADD COLUMN composite_path VARCHAR(512) DEFAULT NULL",
// Phase 5.9: Materialliste pro Auftrag
"CREATE TABLE IF NOT EXISTS ".$this->db->prefix()."bericht_material ("
."rowid INT AUTO_INCREMENT PRIMARY KEY,"
."element_type VARCHAR(32) NOT NULL,"
."fk_element INT NOT NULL,"
."label VARCHAR(255) NOT NULL,"
."qty FLOAT DEFAULT 1,"
."unit VARCHAR(16) DEFAULT 'Stk',"
."note TEXT DEFAULT NULL,"
."fk_user_creat INT NOT NULL,"
."datec DATETIME NOT NULL,"
."INDEX idx_bm_element (element_type, fk_element)"
.") ENGINE=innodb",
);
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);
$fields = array(
'auftragsnummer' => array('label' => 'Auftragsnummer', 'type' => 'varchar', 'size' => 255, 'pos' => 100),
'angebotsnummer' => array('label' => 'Angebotsnummer', 'type' => 'varchar', 'size' => 255, 'pos' => 101),
'rechnungsnummer' => array('label' => 'Rechnungsnummer', 'type' => 'varchar', 'size' => 255, 'pos' => 102),
'beschreibung' => array('label' => 'Auftragsbeschreibung', 'type' => 'text', 'size' => 2000, 'pos' => 103),
'hinweis' => array('label' => 'Hinweis', 'type' => 'varchar', 'size' => 255, 'pos' => 104),
);
foreach ($fields as $name => $def) {
// Existiert das Feld bereits? → nicht überschreiben
$check = $this->db->query("SELECT rowid FROM ".$this->db->prefix()."extrafields"
." WHERE name = '".$this->db->escape($name)."'"
." AND elementtype = 'facture'");
if ($check && $this->db->num_rows($check) > 0) {
continue;
}
$extrafields->addExtraField(
$name,
$def['label'],
$def['type'],
$def['pos'],
$def['size'],
'facture',
0, 0, '', '', 1, '', 0, 0, '', '', 'bericht@bericht', '1'
);
}
$sql = array();
return $this->_init($sql, $options);
}
/**
* Beim Deaktivieren: Konstanten/Permissions entfernen, Daten und Extrafields BLEIBEN erhalten.
*/
public function remove($options = '')
{
$sql = array();
return $this->_remove($sql, $options);
}
}