All checks were successful
Deploy bericht / deploy (push) Successful in 1s
Phase 2.1 Token-System: - Neue Tabelle llx_bericht_upload_token (token, fk_bericht, expires_at, uploads_count, max_uploads) - BerichtUploadToken-Klasse mit create/fetchValid/incrementCount/cleanupExpired - Cronjob 'Bericht: Expired Upload-Tokens bereinigen' täglich - 64-Hex random_bytes-Tokens, 1h Lifetime, 100 Uploads max Phase 2.2 QR-Upload Lite: - mobile_upload.php — Mobile-optimierte Page ohne Dolibarr-Login, Auth nur über Token in URL/Form - 📷 Foto aufnehmen (capture=environment) und 📂 Galerie - Clientseitiges Resize auf max 2000px (Canvas, JPEG q=0.85) - Upload-Status mit Toast-Notifications - Liste der hochgeladenen Bilder live in der Page - ajax/create_upload_token.php — generiert Token für aktiven Bericht - ajax/list_pages.php — Polling-Endpoint für Editor - 📱 Mobil hochladen-Button im Editor → QR-Modal mit qrcodejs - Polling alle 5s nach neuen Pages, auto-reload bei Änderung - QR-Modal styled für Dark-Theme Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> [deploy]
532 lines
28 KiB
PHP
532 lines
28 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: Bericht zur aktuellen Karte verknüpfen
|
|
if ($action === 'link' && $berichtid > 0 && $user->hasRight('bericht', 'write')) {
|
|
$b = new Bericht($db);
|
|
if ($b->fetch($berichtid) > 0) {
|
|
$b->linkToElement($element, $parent->id);
|
|
setEventMessages('Bericht der Rechnung zugeordnet', null, 'mesgs');
|
|
}
|
|
header("Location: ".$_SERVER['PHP_SELF'].'?id='.$parent->id.'&element='.$element);
|
|
exit;
|
|
}
|
|
|
|
if ($action === 'unlink' && $berichtid > 0 && $user->hasRight('bericht', 'write')) {
|
|
$b = new Bericht($db);
|
|
if ($b->fetch($berichtid) > 0) {
|
|
$b->unlinkFromElement($element, $parent->id);
|
|
setEventMessages('Verknüpfung entfernt', null, 'mesgs');
|
|
}
|
|
header("Location: ".$_SERVER['PHP_SELF'].'?id='.$parent->id.'&element='.$element);
|
|
exit;
|
|
}
|
|
|
|
// 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
|
|
* - Berichte direkt zugeordnet (element_type+fk_element)
|
|
* - Berichte zusätzlich verknüpft (llx_element_element)
|
|
* - NUR auf Rechnung: Berichte aus den verknüpften Aufträgen (read-only Sicht)
|
|
*/
|
|
$list_direct = Bericht::fetchAllForElement($db, $element, $parent->id);
|
|
$list_linked = Bericht::fetchLinkedForElement($db, $element, $parent->id);
|
|
|
|
// Aus verknüpften Aufträgen: nur wenn aktuelle Karte eine Rechnung ist
|
|
$list_from_orders = array();
|
|
if ($element === 'invoice') {
|
|
$parent->fetchObjectLinked();
|
|
if (!empty($parent->linkedObjects['commande'])) {
|
|
foreach ($parent->linkedObjects['commande'] as $linked_order) {
|
|
$more = Bericht::fetchAllForElement($db, 'order', $linked_order->id);
|
|
foreach ($more as $b) {
|
|
// Nicht doppelt zeigen wenn schon manuell verknüpft
|
|
$b->_source_order_ref = $linked_order->ref;
|
|
$b->_source_order_id = $linked_order->id;
|
|
$list_from_orders[] = $b;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set der bereits verknüpften IDs für Vergleich
|
|
$linked_ids = array_map(function ($b) { return (int) $b->id; }, $list_linked);
|
|
|
|
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>';
|
|
|
|
/* ----- Helper: Bericht-Tabelle rendern ----- */
|
|
$renderBerichtTable = function ($title, $items, $mode = 'direct') use ($langs, $user, $element, $parent) {
|
|
if (empty($items)) return;
|
|
print '<h4 style="margin-top:16px;margin-bottom:6px;opacity:0.8;">'.$title.' ('.count($items).')</h4>';
|
|
print '<table class="noborder centpercent">';
|
|
print '<tr class="liste_titre">';
|
|
print '<th>'.$langs->trans("Ref").'</th>';
|
|
print '<th>'.$langs->trans("BerichtTitle").'</th>';
|
|
if ($mode === 'from_order') print '<th>Aus Auftrag</th>';
|
|
print '<th>'.$langs->trans("BerichtCreatedAt").'</th>';
|
|
print '<th>'.$langs->trans("BerichtStatus").'</th>';
|
|
print '<th class="right">'.$langs->trans("Action").'</th>';
|
|
print '</tr>';
|
|
foreach ($items 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>';
|
|
if ($mode === 'from_order') {
|
|
print '<td>'.dol_escape_htmltag($b->_source_order_ref ?? '').'</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 ($mode === 'from_order' && $user->hasRight('bericht', 'write')) {
|
|
print '<a href="'.$_SERVER['PHP_SELF'].'?action=link&berichtid='.$b->id.'&id='.$parent->id.'&element='.$element.'&token='.newToken().'" '
|
|
.'class="button-small" title="Diesen Bericht zusätzlich der aktuellen Karte zuordnen">→ Übernehmen</a> ';
|
|
}
|
|
if ($mode === 'linked' && $user->hasRight('bericht', 'write')) {
|
|
print '<a href="'.$_SERVER['PHP_SELF'].'?action=unlink&berichtid='.$b->id.'&id='.$parent->id.'&element='.$element.'&token='.newToken().'" '
|
|
.'class="button-small" title="Verknüpfung lösen">⊗ Lösen</a> ';
|
|
}
|
|
if ($mode !== 'from_order' && $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>';
|
|
};
|
|
|
|
if (empty($list_direct) && empty($list_linked) && empty($list_from_orders)) {
|
|
print '<div class="opacitymedium" style="padding:20px;">'.$langs->trans("BerichtNoReports").'</div>';
|
|
} else {
|
|
$renderBerichtTable('Berichte direkt zu dieser Karte', $list_direct, 'direct');
|
|
$renderBerichtTable('Zusätzlich verknüpfte Berichte', $list_linked, 'linked');
|
|
if ($element === 'invoice') {
|
|
// Auftragsberichte filtern: solche die schon verknüpft sind, ausblenden
|
|
$remaining = array_filter($list_from_orders, function ($b) use ($linked_ids) {
|
|
return !in_array((int) $b->id, $linked_ids, true);
|
|
});
|
|
$renderBerichtTable('Berichte aus verknüpften Aufträgen', $remaining, 'from_order');
|
|
}
|
|
}
|
|
|
|
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),
|
|
'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),
|
|
'save_page_options'=> dol_buildpath('/bericht/ajax/save_page_options.php', 1),
|
|
'create_grid_page' => dol_buildpath('/bericht/ajax/create_grid_page.php', 1),
|
|
'set_slot_image' => dol_buildpath('/bericht/ajax/set_slot_image.php', 1),
|
|
'create_upload_token' => dol_buildpath('/bericht/ajax/create_upload_token.php', 1),
|
|
'list_pages' => dol_buildpath('/bericht/ajax/list_pages.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><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 ' ';
|
|
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>';
|
|
|
|
// 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 '<div 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']).'" title="Auswählen">';
|
|
print '<span class="att-icon">'.$icon.'</span>';
|
|
print '<span class="att-name" title="'.dol_escape_htmltag($f['filename']).'">'.dol_escape_htmltag($f['filename']).'</span>';
|
|
print '<span class="att-size opacitymedium small">'.dol_print_size($f['size']).'</span>';
|
|
if ($user->hasRight('bericht', 'write')) {
|
|
print '<button type="button" class="att-delete" data-relpath="'.dol_escape_htmltag($f['relpath']).'" data-source-ref="'.dol_escape_htmltag($f['source_ref']).'" title="Diese Datei aus dem Anhang löschen">🗑️</button>';
|
|
}
|
|
print '</div>';
|
|
}
|
|
print '</div>';
|
|
}
|
|
print '<button type="button" id="btn-add-selected" class="butAction" title="Jedes ausgewählte Bild als eigene Seite">'.$langs->trans("BerichtAddSelectedToReport").'</button>';
|
|
print '<div class="bericht-grid-buttons" style="margin-top:6px;">';
|
|
print '<span class="opacitymedium small">Mehrere Bilder als Grid:</span><br>';
|
|
print '<button type="button" class="btn-add-grid" data-layout="grid_2" title="2 Bilder (oben/unten)">▭▭</button> ';
|
|
print '<button type="button" class="btn-add-grid" data-layout="grid_2v" title="2 Bilder (links/rechts)">▯▯</button> ';
|
|
print '<button type="button" class="btn-add-grid" data-layout="grid_4" title="2x2 Grid">▦</button> ';
|
|
print '<button type="button" class="btn-add-grid" data-layout="grid_6" title="3x2 Grid">▦▦</button>';
|
|
print '</div>';
|
|
}
|
|
print '<hr>';
|
|
print '<div class="bericht-upload">';
|
|
print '<label for="bericht-extra-upload" class="butAction" title="Datei vom PC hochladen">📤 '.$langs->trans("BerichtUploadExtra").'</label>';
|
|
print '<input type="file" id="bericht-extra-upload" style="display:none" accept=".pdf,.png,.jpg,.jpeg">';
|
|
print '<button type="button" id="btn-show-qr" class="butAction" title="QR-Code für Mobile-Upload erzeugen" style="margin-top:6px;">📱 Mobil hochladen</button>';
|
|
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>';
|
|
|
|
// ====== Dritte Zeile: Layout & Bildgröße der aktuellen Seite ======
|
|
print '<div class="row-break"></div>';
|
|
print '<label title="Seiten-Layout: Single = 1 Bild, Grid = mehrere Bilder">Layout: <select id="page-layout">'
|
|
.'<option value="single">Single (1 Bild)</option>'
|
|
.'<option value="grid_2">2 Bilder (oben/unten)</option>'
|
|
.'<option value="grid_2v">2 Bilder (links/rechts)</option>'
|
|
.'<option value="grid_4">4 Bilder (2x2)</option>'
|
|
.'<option value="grid_6">6 Bilder (3x2)</option>'
|
|
.'</select></label>';
|
|
print '<label class="single-only" title="Wie groß das Bild auf der Seite gedruckt wird (nur Single)">Größe: <select id="page-imgscale">'
|
|
.'<option value="1.0">Vollseite (100%)</option>'
|
|
.'<option value="0.7">Groß (70%)</option>'
|
|
.'<option value="0.5">Mittel (50%)</option>'
|
|
.'<option value="0.3">Klein (30%)</option>'
|
|
.'</select></label>';
|
|
print '<label class="single-only" title="Position auf der Seite (nur bei kleineren Bildern)">Position: <select id="page-imgalign">'
|
|
.'<option value="fit">Anpassen</option>'
|
|
.'<option value="center">Zentriert</option>'
|
|
.'<option value="topleft">Oben links</option>'
|
|
.'<option value="topright">Oben rechts</option>'
|
|
.'<option value="bottomleft">Unten links</option>'
|
|
.'<option value="bottomright">Unten rechts</option>'
|
|
.'</select></label>';
|
|
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" title="Aktuellen Stand als Entwurf speichern">💾 '.$langs->trans("BerichtSaveDraft").'</button>';
|
|
print '<button type="button" id="btn-preview" class="butAction" title="PDF-Vorschau ansehen ohne zu finalisieren">👁️ Vorschau</button>';
|
|
print '<button type="button" id="btn-finalize" class="butActionConfirm" title="PDF erzeugen und unter Verknüpfte Dokumente ablegen">📑 '.$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-Vorschau-Modal
|
|
print '<div id="bericht-preview-modal" class="bericht-modal" style="display:none;">';
|
|
print ' <div class="bericht-modal-backdrop"></div>';
|
|
print ' <div class="bericht-modal-content">';
|
|
print ' <div class="bericht-modal-header">';
|
|
print ' <h3>📑 PDF-Vorschau</h3>';
|
|
print ' <button type="button" id="bericht-modal-close" title="Schließen">✕</button>';
|
|
print ' </div>';
|
|
print ' <div class="bericht-modal-body">';
|
|
print ' <iframe id="bericht-preview-iframe" src="about:blank"></iframe>';
|
|
print ' </div>';
|
|
print ' </div>';
|
|
print '</div>';
|
|
|
|
// QR-Code-Modal für Mobile-Upload
|
|
print '<div id="bericht-qr-modal" class="bericht-modal" style="display:none;">';
|
|
print ' <div class="bericht-modal-backdrop"></div>';
|
|
print ' <div class="bericht-modal-content qr-modal">';
|
|
print ' <div class="bericht-modal-header">';
|
|
print ' <h3>📱 Mobile-Upload</h3>';
|
|
print ' <button type="button" id="bericht-qr-close" title="Schließen">✕</button>';
|
|
print ' </div>';
|
|
print ' <div class="bericht-modal-body qr-modal-body">';
|
|
print ' <div id="qr-code-container"></div>';
|
|
print ' <div class="qr-info">';
|
|
print ' <p>Scanne den QR-Code mit deinem Handy, um direkt Fotos in diesen Bericht hochzuladen.</p>';
|
|
print ' <p class="opacitymedium small">Token gültig: <span id="qr-validity">—</span> Min</p>';
|
|
print ' <p class="qr-url-display"><a id="qr-url-link" href="" target="_blank">Link öffnen</a></p>';
|
|
print ' <p class="qr-status opacitymedium small">Warte auf Uploads…</p>';
|
|
print ' </div>';
|
|
print ' </div>';
|
|
print ' </div>';
|
|
print '</div>';
|
|
|
|
// 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 src="'.dol_buildpath('/bericht/js/lib/qrcode.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();
|