bericht/bericht_card.php
Eduard Wisch a7a533f3b8
All checks were successful
Deploy bericht / deploy (push) Successful in 1s
feat: Toolbar-Einstellungen merken (localStorage) + Tooltips überall
- Farbe, Strichstärke, Schriftart, Schriftgröße, Bold/Italic und Zoom
  werden in localStorage unter bericht.editor.settings.v1 gespeichert
- Beim nächsten Öffnen werden alle Werte wiederhergestellt
- Alle Toolbar-Buttons und Inputs haben jetzt deutsche Tooltips
  (Farbe, Strichstärke, Schriftart, Größe, Fett, Kursiv, Zoom -/+/Reset,
   Rotation links/rechts)

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

362 lines
18 KiB
PHP

<?php
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
* GPL v3+
*
* Editor-Seite des Bericht-Moduls.
* Eingang über Tab "Bericht" auf Rechnung/Auftrag/Angebot.
*
* Aufruf:
* /bericht/bericht_card.php?id=<parent_id>&element=invoice|order|propal
* /bericht/bericht_card.php?berichtid=<bericht_id> (direkter Aufruf eines bestehenden Berichts)
*/
// Standard-Dolibarr-Include-Kaskade (symlink-sicher)
$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.'/core/lib/functions2.lib.php';
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", "other"));
$id = GETPOSTINT('id');
$berichtid = GETPOSTINT('berichtid');
$element = GETPOST('element', 'alpha');
$action = GETPOST('action', 'alpha');
// Bericht laden bzw. Parent ermitteln
$bericht = null;
$parent = null;
if ($berichtid > 0) {
$bericht = new Bericht($db);
if ($bericht->fetch($berichtid) <= 0) {
accessforbidden('Bericht nicht gefunden');
}
$element = $bericht->element_type;
$id = $bericht->fk_element;
}
$parent = bericht_fetch_parent($db, $element, $id);
if (!$parent) {
setEventMessages($langs->trans("BerichtErrorNoParent"), null, 'errors');
llxHeader('', $langs->trans("Bericht"));
print '<div class="error">'.$langs->trans("BerichtErrorNoParent").'</div>';
llxFooter();
exit;
}
$auftragsnummer = bericht_get_auftragsnummer($parent);
// Aktion: neuen Bericht anlegen
if ($action === 'create' && $user->hasRight('bericht', 'write')) {
$b = new Bericht($db);
$b->element_type = $element;
$b->fk_element = $parent->id;
$b->titel = GETPOST('titel', 'alphanohtml') ?: ('Bericht '.$auftragsnummer);
$b->auftragsnummer = $auftragsnummer;
$b->template_odt = getDolGlobalString('BERICHT_DEFAULT_TEMPLATE', '');
if ($b->create($user) > 0) {
header("Location: ".$_SERVER['PHP_SELF'].'?berichtid='.$b->id);
exit;
}
setEventMessages($b->error, $b->errors, 'errors');
}
// Aktion: Bericht löschen
if ($action === 'delete' && $berichtid > 0 && $user->hasRight('bericht', 'delete')) {
if ($bericht->delete($user) > 0) {
setEventMessages($langs->trans("BerichtDeleteSuccess"), null, 'mesgs');
// Zurück auf die Tab-Übersicht (gleiche URL ohne berichtid)
header("Location: ".$_SERVER['PHP_SELF'].'?id='.$parent->id.'&element='.$element);
exit;
}
setEventMessages($bericht->error, $bericht->errors, 'errors');
}
/*
* Anzeige
*/
$title = $langs->trans("Bericht").' — '.$parent->ref;
llxHeader('', $title, '', '', 0, 0, array(), array(), '', 'mod-bericht page-bericht-card');
// Header des Parent-Objekts (Standard-Dolibarr-Tabs für Rechnung/Auftrag/Angebot)
if ($element === 'invoice') {
require_once DOL_DOCUMENT_ROOT.'/core/lib/invoice.lib.php';
$head = facture_prepare_head($parent);
print dol_get_fiche_head($head, 'bericht', $langs->trans("Bill"), -1, 'bill');
} elseif ($element === 'order') {
require_once DOL_DOCUMENT_ROOT.'/core/lib/order.lib.php';
$head = commande_prepare_head($parent);
print dol_get_fiche_head($head, 'bericht', $langs->trans("CustomerOrder"), -1, 'order');
} elseif ($element === 'propal') {
require_once DOL_DOCUMENT_ROOT.'/core/lib/propal.lib.php';
$head = propal_prepare_head($parent);
print dol_get_fiche_head($head, 'bericht', $langs->trans("Proposal"), -1, 'propal');
}
// Banner mit Parent-Infos
$linkback = '<a href="javascript:history.back()">'.$langs->trans("BackToList").'</a>';
dol_banner_tab($parent, 'ref', $linkback, 1, 'ref');
print dol_get_fiche_end();
print '<br>';
if (!$bericht) {
/*
* MODUS A: Übersicht — Liste vorhandener Berichte + "Neu anlegen"
*/
$list = Bericht::fetchAllForElement($db, $element, $parent->id);
print '<div class="bericht-overview">';
print '<div class="bericht-overview-header">';
print '<h3>'.$langs->trans("Berichte").'</h3>';
if ($user->hasRight('bericht', 'write')) {
print '<form method="post" class="inline-block">';
print '<input type="hidden" name="token" value="'.newToken().'">';
print '<input type="hidden" name="action" value="create">';
print '<input type="hidden" name="id" value="'.$parent->id.'">';
print '<input type="hidden" name="element" value="'.$element.'">';
print '<button type="submit" class="butAction">+ '.$langs->trans("BerichtNew").'</button>';
print '</form>';
}
print '</div>';
if (empty($list)) {
print '<div class="opacitymedium" style="padding:20px;">'.$langs->trans("BerichtNoReports").'</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>'.$langs->trans("BerichtCreatedAt").'</th>';
print '<th>'.$langs->trans("BerichtStatus").'</th>';
print '<th class="right">'.$langs->trans("Action").'</th>';
print '</tr>';
foreach ($list as $b) {
$url = $_SERVER['PHP_SELF'].'?berichtid='.$b->id;
print '<tr class="oddeven">';
print '<td><a href="'.$url.'">'.dol_escape_htmltag($b->ref).'</a></td>';
print '<td>'.dol_escape_htmltag($b->titel).'</td>';
print '<td>'.dol_print_date($b->datec, 'dayhour').'</td>';
print '<td>'.$b->getLibStatut().'</td>';
print '<td class="right">';
print '<a href="'.$url.'" class="button-small">'.$langs->trans("Open").'</a> ';
if ($user->hasRight('bericht', 'delete')) {
print '<a href="'.$_SERVER['PHP_SELF'].'?action=delete&berichtid='.$b->id.'&token='.newToken().'" '
.'onclick="return confirm(\''.dol_escape_js($langs->trans("BerichtConfirmDelete")).'\')" '
.'class="button-small button-delete">'.$langs->trans("Delete").'</a>';
}
print '</td>';
print '</tr>';
}
print '</table>';
}
print '</div>';
} else {
/*
* MODUS B: Editor — bestehender Bericht wird bearbeitet
*/
$pages = BerichtPage::fetchAllForBericht($db, $bericht->id);
$attachments = bericht_collect_attachments($db, $parent, $element);
$templates = bericht_list_templates();
// Daten für JS bereitstellen
$editor_config = array(
'berichtid' => (int) $bericht->id,
'token' => newToken(),
'urls' => array(
'save_annotations' => dol_buildpath('/bericht/ajax/save_annotations.php', 1),
'upload_extra' => dol_buildpath('/bericht/ajax/upload_extra.php', 1),
'add_attachment' => dol_buildpath('/bericht/ajax/add_attachment.php', 1),
'delete_page' => dol_buildpath('/bericht/ajax/delete_page.php', 1),
'reorder_pages' => dol_buildpath('/bericht/ajax/reorder_pages.php', 1),
'page_image' => dol_buildpath('/bericht/ajax/page_image.php', 1),
'generate_pdf' => dol_buildpath('/bericht/ajax/generate_pdf.php', 1),
),
'lang' => array(
'undo' => $langs->trans("BerichtUndo"),
'redo' => $langs->trans("BerichtRedo"),
'select' => $langs->trans("BerichtToolSelect"),
'draw' => $langs->trans("BerichtToolDraw"),
'rect' => $langs->trans("BerichtToolRect"),
'circle' => $langs->trans("BerichtToolCircle"),
'arrow' => $langs->trans("BerichtToolArrow"),
'text' => $langs->trans("BerichtToolText"),
'delete' => $langs->trans("BerichtToolDelete"),
'note_hint' => $langs->trans("BerichtNoteHint"),
'confirm_del' => $langs->trans("BerichtConfirmDelete"),
),
);
print '<div class="bericht-editor">';
// Kopfzeile mit Bericht-Meta
print '<div class="bericht-meta">';
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.'">';
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("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>';
print '<td>'.$langs->trans("BerichtTemplate").'</td><td><select name="template_odt">';
print '<option value="">— '.$langs->trans("BerichtNoTemplate").' —</option>';
foreach ($templates as $tpl) {
$sel = ($tpl === $bericht->template_odt) ? ' selected' : '';
print '<option value="'.dol_escape_htmltag($tpl).'"'.$sel.'>'.dol_escape_htmltag($tpl).'</option>';
}
print '</select></td>';
print '</tr></table>';
print '</form>';
print '</div>';
// Hauptlayout: links Anhänge, Mitte Editor, rechts Seiten
print '<div class="bericht-layout">';
// LINKS: Anhänge-Browser
print '<aside class="bericht-attachments">';
print '<h4>'.$langs->trans("BerichtAvailableAttachments").'</h4>';
$by_source = array();
foreach ($attachments as $a) {
$key = $a['source'].':'.$a['source_ref'];
$by_source[$key][] = $a;
}
if (empty($by_source)) {
print '<div class="opacitymedium small">'.$langs->trans("None").'</div>';
} else {
foreach ($by_source as $key => $files) {
list($src, $ref) = explode(':', $key, 2);
print '<div class="bericht-att-group"><div class="bericht-att-group-title">'.dol_escape_htmltag($ref).'</div>';
foreach ($files as $f) {
$icon = (strpos($f['mime'], 'image') === 0) ? '🖼' : ((strpos($f['mime'], 'pdf') !== false) ? '📄' : '📎');
print '<label class="bericht-att-item">';
print '<input type="checkbox" class="att-check" data-relpath="'.dol_escape_htmltag($f['relpath']).'" data-mime="'.dol_escape_htmltag($f['mime']).'">';
print '<span class="att-icon">'.$icon.'</span>';
print '<span class="att-name">'.dol_escape_htmltag($f['filename']).'</span>';
print '<span class="att-size opacitymedium small">'.dol_print_size($f['size']).'</span>';
print '</label>';
}
print '</div>';
}
print '<button type="button" id="btn-add-selected" class="butAction">'.$langs->trans("BerichtAddSelectedToReport").'</button>';
}
print '<hr>';
print '<div class="bericht-upload">';
print '<label for="bericht-extra-upload" class="butAction">📤 '.$langs->trans("BerichtUploadExtra").'</label>';
print '<input type="file" id="bericht-extra-upload" style="display:none" accept=".pdf,.png,.jpg,.jpeg">';
print '</div>';
print '</aside>';
// MITTE: PDF.js + Fabric.js Editor
print '<main class="bericht-canvas-area">';
print '<div class="bericht-toolbar">';
print '<button type="button" class="tool-btn" data-tool="select" title="'.$langs->trans("BerichtToolSelect").'">↖</button>';
print '<button type="button" class="tool-btn" data-tool="draw" title="'.$langs->trans("BerichtToolDraw").'">✏️</button>';
print '<button type="button" class="tool-btn" data-tool="rect" title="'.$langs->trans("BerichtToolRect").'">▭</button>';
print '<button type="button" class="tool-btn" data-tool="circle" title="'.$langs->trans("BerichtToolCircle").'">○</button>';
print '<button type="button" class="tool-btn" data-tool="arrow" title="'.$langs->trans("BerichtToolArrow").'">↗</button>';
print '<button type="button" class="tool-btn" data-tool="text" title="'.$langs->trans("BerichtToolText").'">T</button>';
print '<span class="sep"></span>';
print '<label title="Linien-/Textfarbe">'.$langs->trans("BerichtColor").': <input type="color" id="tool-color" value="#ff0000" title="Farbe wählen"></label>';
print '<label title="Linien-/Strichstärke in Pixel">'.$langs->trans("BerichtStrokeWidth").': <input type="range" id="tool-stroke" min="1" max="20" value="3" title="Strichstärke"></label>';
print '<span class="sep"></span>';
print '<button type="button" id="btn-undo" title="'.$langs->trans("BerichtUndo").'">↶</button>';
print '<button type="button" id="btn-redo" title="'.$langs->trans("BerichtRedo").'">↷</button>';
print '<button type="button" id="btn-delete-selected" title="'.$langs->trans("BerichtToolDelete").'">🗑️</button>';
// ====== Zweite Zeile ======
print '<div class="row-break"></div>';
// Text-Optionen
print '<label class="text-tool-option" title="Schriftart für Text-Annotationen">Schrift: <select id="tool-fontfamily" title="Schriftart">'
.'<option value="Helvetica">Helvetica</option>'
.'<option value="Arial">Arial</option>'
.'<option value="Times New Roman">Times New Roman</option>'
.'<option value="Courier New">Courier New</option>'
.'<option value="Verdana">Verdana</option>'
.'<option value="Georgia">Georgia</option>'
.'</select></label>';
print '<label class="text-tool-option" title="Schriftgröße in Pixel">Größe: <input type="number" id="tool-fontsize" min="8" max="120" value="24" style="width:60px" title="Schriftgröße"></label>';
print '<label class="text-tool-option" title="Fett"><input type="checkbox" id="tool-bold" title="Fett"> <b>B</b></label>';
print '<label class="text-tool-option" title="Kursiv"><input type="checkbox" id="tool-italic" title="Kursiv"> <i>I</i></label>';
print '<span class="sep"></span>';
// Zoom
print '<button type="button" id="btn-zoom-out" title="Verkleinern (Zoom -)">🔍−</button>';
print '<span id="zoom-label" title="Aktueller Zoom" style="min-width:42px;text-align:center;font-size:12px;">100%</span>';
print '<button type="button" id="btn-zoom-in" title="Vergrößern (Zoom +)">🔍+</button>';
print '<button type="button" id="btn-zoom-reset" title="Zoom auf 100% zurücksetzen">⟳%</button>';
print '<span class="sep"></span>';
print '<button type="button" id="btn-rotate-left" title="'.$langs->trans("BerichtRotateLeft").' (Seite 90° nach links drehen)">⟲</button>';
print '<button type="button" id="btn-rotate-right" title="'.$langs->trans("BerichtRotateRight").' (Seite 90° nach rechts drehen)">⟳</button>';
print '</div>';
print '<div class="bericht-canvas-wrap">';
print '<canvas id="pdf-canvas"></canvas>';
print '<canvas id="fabric-canvas" class="fabric-overlay"></canvas>';
print '</div>';
print '<div class="bericht-page-note">';
print '<label>'.$langs->trans("BerichtNoteHint").'</label>';
print '<textarea id="page-note" rows="2"></textarea>';
print '</div>';
print '</main>';
// RECHTS: Seiten-Thumbnails
print '<aside class="bericht-pages">';
print '<div class="bericht-pages-header">';
print '<h4>'.$langs->trans("BerichtPages").' (<span id="page-count">'.count($pages).'</span>)</h4>';
print '<button type="button" id="btn-toggle-thumb-bg" title="Hell/Dunkel">🌓</button>';
print '</div>';
print '<div id="bericht-page-list" class="page-list paper-light">';
foreach ($pages as $idx => $p) {
print '<div class="page-thumb" data-pageid="'.$p->id.'" data-order="'.$p->page_order.'">';
print '<div class="page-thumb-paper">';
print '<canvas class="thumb-canvas"></canvas>';
print '</div>';
print '<div class="page-thumb-label"><span class="page-num">'.($idx + 1).'</span></div>';
print '<div class="page-thumb-actions">';
print '<button type="button" class="thumb-del" title="'.$langs->trans("BerichtDeletePage").'">🗑️</button>';
print '</div>';
print '</div>';
}
print '</div>';
print '</aside>';
print '</div>'; // .bericht-layout
// Footer-Aktionen
print '<div class="bericht-actions">';
print '<button type="button" id="btn-save-draft" class="butAction">💾 '.$langs->trans("BerichtSaveDraft").'</button>';
print '<button type="button" id="btn-finalize" class="butActionConfirm">📑 '.$langs->trans("BerichtFinalize").'</button>';
if ($user->hasRight('bericht', 'delete')) {
print '<a href="'.$_SERVER['PHP_SELF'].'?action=delete&berichtid='.$bericht->id.'&token='.newToken().'" '
.'onclick="return confirm(\''.dol_escape_js($langs->trans("BerichtConfirmDelete")).'\')" '
.'class="butActionDelete">🗑️ '.$langs->trans("Delete").'</a>';
}
print '</div>';
print '</div>'; // .bericht-editor
// PDF.js + Fabric.js (lokal)
print '<script src="'.dol_buildpath('/bericht/js/lib/pdf.min.js', 1).'"></script>';
print '<script src="'.dol_buildpath('/bericht/js/lib/fabric.min.js', 1).'"></script>';
print '<script src="'.dol_buildpath('/bericht/js/lib/Sortable.min.js', 1).'"></script>';
print '<script>window.BERICHT_CONFIG = '.json_encode($editor_config).';</script>';
print '<script src="'.dol_buildpath('/bericht/js/editor.js', 1).'"></script>';
}
llxFooter();
$db->close();