All checks were successful
Deploy bericht / deploy (push) Successful in 6s
- Neuer API-Endpoint api/shipments.php: Liste Lieferungen zu Auftrag, PDF-Stream, confirm (Unterschrift stempeln)
- ODT-Hook actions_bericht.class.php: ersetzt {signature} Platzhalter via odfphp->setImage, setzt {signer_name}/{signed_at}/{gps}
- Backup-Roundtrip: generateDocument-Backup → signed.pdf erzeugen → Original wiederherstellen
- JWT-Fallback in _jwt.php: ?jwt= Query-Param für <img>/<object> ohne Authorization-Header
- Admin: BERICHT_SIGNATURE_IMAGE_RATIO Feld, Toggle BERICHT_TAB_ON_SHIPMENT, Signature-Box-Editor
- DB: llx_bericht_signature_box für pro-Template mm-Box-Geometrie
- element_type='shipment' in modBericht + lib/bericht.lib.php
- element_element Richtung: commande=source, shipping=target (fk_target=expedition_id)
- DOL_DATA_ROOT-Auflösung für EXPEDITION_ADDON_PDF_ODT_PATH
- Sprachen: de_DE + en_US mit neuen Schlüsseln für Signatur-Workflow
[deploy]
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
406 lines
21 KiB
PHP
406 lines
21 KiB
PHP
<?php
|
||
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||
* GPL v3+
|
||
*
|
||
* Visueller Editor fuer die Unterschriftsbox auf Lieferschein-PDFs.
|
||
* Laedt ein Beispiel-Lieferschein-PDF (PDF.js), zeigt die aktuelle Box als
|
||
* Fabric.js-Rechteck und speichert die mm-Geometrie in llx_bericht_signature_box.
|
||
*/
|
||
|
||
$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 __DIR__.'/../lib/bericht.lib.php';
|
||
require_once DOL_DOCUMENT_ROOT.'/expedition/class/expedition.class.php';
|
||
|
||
if (!$user->admin && !$user->hasRight('bericht', 'admin')) accessforbidden();
|
||
$langs->loadLangs(array("admin", "bericht@bericht"));
|
||
|
||
// Verfuegbare Templates: PDF-Module + ODT-Module + ODT-Dateien aus doctemplates/shipments/
|
||
$tpl_dir = DOL_DOCUMENT_ROOT.'/core/modules/expedition/doc';
|
||
$pdf_modules = array(); // ['espadon' => 'espadon', ...]
|
||
$odt_modules = array(); // ['generic_shipment_odt' => 'generic_shipment_odt']
|
||
if (is_dir($tpl_dir)) {
|
||
foreach (glob($tpl_dir.'/pdf_*.modules.php') as $f) {
|
||
$name = basename($f);
|
||
if (preg_match('/^pdf_(.+)\.modules\.php$/', $name, $m)) {
|
||
$pdf_modules[$m[1]] = $m[1];
|
||
}
|
||
}
|
||
foreach (glob($tpl_dir.'/doc_*.modules.php') as $f) {
|
||
$name = basename($f);
|
||
if (preg_match('/^doc_(.+)\.modules\.php$/', $name, $m)) {
|
||
$odt_modules[$m[1]] = $m[1];
|
||
}
|
||
}
|
||
}
|
||
|
||
// ODT-Dateien aus doctemplates/shipments/ — pro Datei eine eigene Box konfigurierbar
|
||
// EXPEDITION_ADDON_PDF_ODT_PATH enthaelt 'DOL_DATA_ROOT' woertlich + ist komma/zeilengetrennt
|
||
$odt_files = array();
|
||
$raw_dirs = getDolGlobalString('EXPEDITION_ADDON_PDF_ODT_PATH', 'DOL_DATA_ROOT/doctemplates/shipments');
|
||
foreach (explode(',', preg_replace('/[\r\n]+/', ',', trim($raw_dirs))) as $d) {
|
||
$d = trim($d);
|
||
if (!$d) continue;
|
||
$d = preg_replace('/DOL_DATA_ROOT/', DOL_DATA_ROOT, $d);
|
||
if (!is_dir($d)) continue;
|
||
foreach (glob($d.'/*.odt') as $f) {
|
||
$name = basename($f);
|
||
$key = 'odt:'.preg_replace('/\.odt$/i', '', $name);
|
||
$odt_files[$key] = $name;
|
||
}
|
||
}
|
||
|
||
$active_template = getDolGlobalString('EXPEDITION_ADDON_PDF', 'merou');
|
||
$selected = GETPOST('template', 'alphanohtml') ?: $active_template;
|
||
|
||
// Wenn nichts uebergeben + es gibt ODTs + Aktives Modul ist generic_shipment_odt → erstes ODT vorauswaehlen
|
||
if (!GETPOST('template', 'alphanohtml') && $active_template === 'generic_shipment_odt' && !empty($odt_files)) {
|
||
$selected = array_key_first($odt_files);
|
||
}
|
||
|
||
// Auswahl validieren
|
||
$all_choices = array_merge($pdf_modules, $odt_modules, $odt_files);
|
||
if (!isset($all_choices[$selected])) {
|
||
$selected = $active_template;
|
||
if (!isset($all_choices[$selected]) && !empty($all_choices)) {
|
||
$selected = array_key_first($all_choices);
|
||
}
|
||
}
|
||
|
||
// Aktuelle Box-Geometrie fuer das ausgewaehlte Template laden (oder Default)
|
||
$box = bericht_get_signature_box($db, $selected);
|
||
|
||
// Letzte vorhandene Expedition fuer Preview suchen — sonst Hinweis ausgeben
|
||
$preview_shipment_id = 0;
|
||
$r = $db->query("SELECT rowid FROM ".$db->prefix()."expedition"
|
||
." WHERE entity IN (".getEntity('expedition').")"
|
||
." ORDER BY rowid DESC LIMIT 1");
|
||
if ($r && ($o = $db->fetch_object($r))) $preview_shipment_id = (int) $o->rowid;
|
||
|
||
llxHeader('', $langs->trans("BerichtSignatureBoxConfig"));
|
||
|
||
$linkback = '<a href="'.dol_buildpath('/bericht/admin/setup.php', 1).'">'.$langs->trans("BackToSetup").'</a>';
|
||
print load_fiche_titre($langs->trans("BerichtSignatureBoxConfig"), $linkback, 'title_setup');
|
||
print '<p class="opacitymedium">'.$langs->trans("BerichtSignatureBoxConfigDesc").'</p>';
|
||
|
||
// Template-Wahl mit Gruppen
|
||
print '<form method="get" style="margin-bottom:16px;">';
|
||
print '<label style="margin-right:8px;">'.$langs->trans("BerichtPdfTemplate").':</label>';
|
||
print '<select name="template" onchange="this.form.submit()">';
|
||
if (!empty($odt_files)) {
|
||
print '<optgroup label="'.dol_escape_htmltag($langs->trans("BerichtOdtFiles")).'">';
|
||
foreach ($odt_files as $key => $filename) {
|
||
$sel = ($key === $selected) ? ' selected' : '';
|
||
$hint = ($active_template === 'generic_shipment_odt') ? ' ★' : '';
|
||
print '<option value="'.dol_escape_htmltag($key).'"'.$sel.'>'.dol_escape_htmltag($filename).$hint.'</option>';
|
||
}
|
||
print '</optgroup>';
|
||
}
|
||
if (!empty($pdf_modules)) {
|
||
print '<optgroup label="'.dol_escape_htmltag($langs->trans("BerichtPdfModules")).'">';
|
||
foreach ($pdf_modules as $tpl) {
|
||
$sel = ($tpl === $selected) ? ' selected' : '';
|
||
$is_active = ($tpl === $active_template) ? ' ★' : '';
|
||
print '<option value="'.dol_escape_htmltag($tpl).'"'.$sel.'>'.dol_escape_htmltag($tpl).$is_active.'</option>';
|
||
}
|
||
print '</optgroup>';
|
||
}
|
||
if (!empty($odt_modules)) {
|
||
print '<optgroup label="'.dol_escape_htmltag($langs->trans("BerichtOdtModules")).'">';
|
||
foreach ($odt_modules as $tpl) {
|
||
$sel = ($tpl === $selected) ? ' selected' : '';
|
||
$is_active = ($tpl === $active_template) ? ' ★' : '';
|
||
print '<option value="'.dol_escape_htmltag($tpl).'"'.$sel.'>'.dol_escape_htmltag($tpl).$is_active.'</option>';
|
||
}
|
||
print '</optgroup>';
|
||
}
|
||
print '</select>';
|
||
print ' <span class="opacitymedium" style="font-size:12px;">★ = aktuell in Dolibarr aktiv</span>';
|
||
print '</form>';
|
||
|
||
// Hinweis bei ODT-Auswahl: mm-Editor ist hier NICHT relevant.
|
||
// Stattdessen: Bild-Platzhalter "signature" im ODT einbauen — die Position waechst dann automatisch mit dem PDF-Inhalt mit.
|
||
$is_odt = (strpos($selected, 'odt:') === 0);
|
||
if ($is_odt) {
|
||
$sig_ratio = getDolGlobalString('BERICHT_SIGNATURE_IMAGE_RATIO', '0.35');
|
||
print '<div class="info" style="background:#1e3a5f;border:1px solid #4080c0;color:#e0e8f0;padding:14px 18px;border-radius:8px;margin-bottom:16px;font-size:14px;line-height:1.6;">';
|
||
print '<h4 style="margin:0 0 8px;color:#fff;">📄 ODT-Template — Platzhalter-Methode (wie EPCQR/{qrcode})</h4>';
|
||
print '<p style="margin:0;">Bei ODT-Vorlagen ist die mm-Box <strong>nicht sinnvoll</strong>: das PDF wird je nach Positionsmenge unterschiedlich lang — fixe Position würde mitten in den Zeilen landen.</p>';
|
||
print '<p style="margin:10px 0 4px;font-weight:600;">Stattdessen:</p>';
|
||
print '<ol style="margin:4px 0 0 18px;padding:0;">';
|
||
print '<li>In deinem ODT (<code>'.dol_escape_htmltag($odt_files[$selected] ?? '').'</code>) an die gewünschte Stelle den Text-Platzhalter <code>{signature}</code> schreiben — irgendwo, wo die Unterschrift erscheinen soll.</li>';
|
||
print '<li>Drumherum optional Text-Variablen einsetzen: <code>{signer_name}</code>, <code>{signed_at}</code>, <code>{gps}</code>, <code>{kunde_name}</code>, <code>{kunde_adresse}</code>, <code>{shipment_ref}</code>, <code>{order_ref}</code>, <code>{auftragsnummer}</code>, <code>{datum}</code>.</li>';
|
||
print '<li>Speichern. Fertig.</li>';
|
||
print '</ol>';
|
||
print '<p style="margin:12px 0 4px;font-size:12px;opacity:0.85;">Beim Bestätigen in der PWA ersetzt <code>setImage()</code> den <code>{signature}</code>-Text durch ein <code><draw:frame></code> mit der Kunden-Unterschrift. Die Größe wird durch den Faktor <code>BERICHT_SIGNATURE_IMAGE_RATIO</code> (aktuell <strong>'.dol_escape_htmltag($sig_ratio).'</strong>) gesteuert — bei 0.35 ergibt das ca. 7×3.5 cm.</p>';
|
||
print '<p style="margin:8px 0 0;font-size:12px;opacity:0.6;">Die mm-Felder rechts sind hier ohne Wirkung — sie gelten nur für reine PDF-Module (espadon/merou/rouget).</p>';
|
||
print '</div>';
|
||
}
|
||
|
||
if (!$preview_shipment_id) {
|
||
print '<div class="warning">'.$langs->trans("BerichtSignatureBoxNoPreviewShipment").'</div>';
|
||
} else {
|
||
// PDF-URL fuer die Vorschau: nutzt den shipments.php-Endpoint mit JWT
|
||
// Hier brauchen wir keinen JWT, weil wir admin sind und das PDF lokal lesen.
|
||
// Stattdessen: Vorschau-Endpoint nimmt nur die Expedition-ID und laedt das PDF.
|
||
$preview_url = dol_buildpath('/bericht/admin/signature_box_preview.php', 1)
|
||
.'?id='.$preview_shipment_id
|
||
.'&template='.urlencode($selected);
|
||
|
||
print '<div class="bericht-setup-section">';
|
||
print '<div style="display:flex;gap:24px;flex-wrap:wrap;align-items:flex-start;">';
|
||
|
||
// Linke Spalte: PDF-Vorschau mit Box
|
||
print '<div style="flex:1;min-width:400px;">';
|
||
print '<h4>'.$langs->trans("BerichtPreviewWithBox").'</h4>';
|
||
print '<style>'
|
||
.'#pdf-container { position:relative; display:inline-block; line-height:0; }'
|
||
// PDF zeichnen-Canvas: bleibt im Fluss
|
||
.'#pdf-container > canvas:first-child { display:block; position:relative; z-index:1; }'
|
||
// Fabric wraps in .canvas-container — der muss absolut OBEN drauf liegen
|
||
.'#pdf-container .canvas-container { position:absolute !important; top:0 !important; left:0 !important; z-index:10 !important; }'
|
||
// Innere Canvas (upper + lower) auch sauber stacked
|
||
.'#pdf-container .canvas-container canvas { position:absolute; top:0; left:0; }'
|
||
.'</style>';
|
||
print '<div id="pdf-stage" style="background:#222;padding:8px;border-radius:6px;display:inline-block;max-width:100%;overflow:auto;">';
|
||
print '<div id="pdf-container"></div>';
|
||
print '</div>';
|
||
print '<div style="margin-top:8px;display:flex;gap:8px;align-items:center;">';
|
||
print '<button type="button" id="btn-prev-page" class="butAction">‹</button>';
|
||
print '<span id="page-info">…</span>';
|
||
print '<button type="button" id="btn-next-page" class="butAction">›</button>';
|
||
print '</div>';
|
||
print '</div>';
|
||
|
||
// Rechte Spalte: Werte editierbar + Speichern (bei ODT optisch gedimmt)
|
||
$right_attr = $is_odt ? ' style="flex:0 0 280px;opacity:0.5;pointer-events:none;"' : ' style="flex:0 0 280px;"';
|
||
print '<div'.$right_attr.'>';
|
||
print '<h4>'.$langs->trans("BerichtBoxGeometry").($is_odt ? ' <span style="font-size:11px;font-weight:normal;">(bei ODT inaktiv)</span>' : '').'</h4>';
|
||
print '<form id="box-form">';
|
||
print '<input type="hidden" name="template_name" value="'.dol_escape_htmltag($selected).'">';
|
||
print '<table class="noborder centpercent">';
|
||
print '<tr><td>'.$langs->trans("BerichtBoxPage").'</td><td><select name="page">';
|
||
foreach (array('last' => $langs->trans("BerichtBoxPageLast"), 'first' => $langs->trans("BerichtBoxPageFirst")) as $v => $l) {
|
||
$sel = ($box['page'] == $v) ? ' selected' : '';
|
||
print '<option value="'.$v.'"'.$sel.'>'.$l.'</option>';
|
||
}
|
||
print '</select></td></tr>';
|
||
print '<tr><td>X (mm)</td><td><input type="number" step="0.5" name="x_mm" id="inp-x" value="'.((float) $box['x_mm']).'"></td></tr>';
|
||
print '<tr><td>Y (mm)</td><td><input type="number" step="0.5" name="y_mm" id="inp-y" value="'.((float) $box['y_mm']).'"></td></tr>';
|
||
print '<tr><td>'.$langs->trans("Width").' (mm)</td><td><input type="number" step="0.5" name="w_mm" id="inp-w" value="'.((float) $box['w_mm']).'"></td></tr>';
|
||
print '<tr><td>'.$langs->trans("Height").' (mm)</td><td><input type="number" step="0.5" name="h_mm" id="inp-h" value="'.((float) $box['h_mm']).'"></td></tr>';
|
||
print '<tr><td>'.$langs->trans("BerichtBoxLabel").'</td><td><input type="text" name="label" value="'.dol_escape_htmltag($box['label']).'"></td></tr>';
|
||
print '</table>';
|
||
print '<br>';
|
||
print '<button type="button" id="btn-save-box" class="butAction">💾 '.$langs->trans("Save").'</button>';
|
||
print '<button type="button" id="btn-reset-box" class="butAction" style="margin-left:8px;">'.$langs->trans("BerichtBoxResetDefault").'</button>';
|
||
print '<div id="save-status" style="margin-top:8px;opacity:0.7;font-size:13px;"></div>';
|
||
print '</form>';
|
||
print '</div>'; // rechte Spalte
|
||
print '</div>'; // Flex
|
||
print '</div>'; // section
|
||
|
||
// PDF.js einbinden + Editor-JS
|
||
$pdfjs_url = dol_buildpath('/bericht/js/lib/pdf.min.js', 1);
|
||
$pdfjs_worker_url = dol_buildpath('/bericht/js/lib/pdf.worker.min.js', 1);
|
||
$fabric_url = dol_buildpath('/bericht/js/lib/fabric.min.js', 1);
|
||
$save_url = dol_buildpath('/bericht/ajax/save_signature_box.php', 1);
|
||
?>
|
||
<script src="<?php echo dol_escape_htmltag($pdfjs_url); ?>"></script>
|
||
<script src="<?php echo dol_escape_htmltag($fabric_url); ?>"></script>
|
||
<script>
|
||
(function () {
|
||
if (window.pdfjsLib) {
|
||
window.pdfjsLib.GlobalWorkerOptions.workerSrc = <?php echo json_encode($pdfjs_worker_url); ?>;
|
||
}
|
||
const PDF_URL = <?php echo json_encode($preview_url); ?>;
|
||
const SAVE_URL = <?php echo json_encode($save_url); ?>;
|
||
const TEMPLATE = <?php echo json_encode($selected); ?>;
|
||
const IS_ODT = <?php echo json_encode($is_odt); ?>;
|
||
const DEFAULT_BOX = <?php echo json_encode(bericht_get_signature_box($db, '')); ?>;
|
||
// 1 PDF-Point = 25.4/72 mm
|
||
const PT_TO_MM = 25.4 / 72.0;
|
||
|
||
let pdfDoc = null, pageNum = 1, pageCount = 0;
|
||
let pageWmm = 210, pageHmm = 297;
|
||
let renderScale = 1.0;
|
||
let fabricCanvas = null, fabricRect = null;
|
||
|
||
const stage = document.getElementById('pdf-container');
|
||
const inpX = document.getElementById('inp-x');
|
||
const inpY = document.getElementById('inp-y');
|
||
const inpW = document.getElementById('inp-w');
|
||
const inpH = document.getElementById('inp-h');
|
||
const pageInfo = document.getElementById('page-info');
|
||
|
||
function chosenPageNum() {
|
||
const pageSel = document.querySelector('select[name="page"]').value;
|
||
if (pageSel === 'first') return 1;
|
||
return pageCount; // last
|
||
}
|
||
|
||
async function loadPdf() {
|
||
const buf = await fetch(PDF_URL, { credentials: 'same-origin' }).then(r => r.arrayBuffer());
|
||
pdfDoc = await pdfjsLib.getDocument({ data: buf }).promise;
|
||
pageCount = pdfDoc.numPages;
|
||
pageNum = chosenPageNum();
|
||
await renderPage();
|
||
}
|
||
|
||
async function renderPage() {
|
||
const page = await pdfDoc.getPage(pageNum);
|
||
const viewport1 = page.getViewport({ scale: 1 });
|
||
// PDF-Punkte → mm
|
||
pageWmm = viewport1.width * PT_TO_MM;
|
||
pageHmm = viewport1.height * PT_TO_MM;
|
||
|
||
// Skala so waehlen, dass max 700px breit
|
||
const targetW = Math.min(700, window.innerWidth - 360);
|
||
renderScale = targetW / viewport1.width;
|
||
const viewport = page.getViewport({ scale: renderScale });
|
||
|
||
// Stage clear + Canvas zeichnen
|
||
stage.innerHTML = '';
|
||
const canvas = document.createElement('canvas');
|
||
canvas.width = viewport.width;
|
||
canvas.height = viewport.height;
|
||
canvas.style.display = 'block';
|
||
stage.appendChild(canvas);
|
||
|
||
const ctx = canvas.getContext('2d');
|
||
await page.render({ canvasContext: ctx, viewport: viewport }).promise;
|
||
|
||
// Fabric-Layer obendrueber
|
||
const fabricEl = document.createElement('canvas');
|
||
fabricEl.width = viewport.width;
|
||
fabricEl.height = viewport.height;
|
||
fabricEl.style.position = 'absolute';
|
||
fabricEl.style.top = '0';
|
||
fabricEl.style.left = '0';
|
||
stage.appendChild(fabricEl);
|
||
|
||
if (fabricCanvas) { try { fabricCanvas.dispose(); } catch (e) {} }
|
||
fabricCanvas = new fabric.Canvas(fabricEl, { selection: false });
|
||
// Container-Geometrie auf PDF-Canvas-Groesse fixieren, damit das Fabric-Overlay
|
||
// (vom CSS '#pdf-container .canvas-container' absolut positioniert) deckt.
|
||
stage.style.height = viewport.height + 'px';
|
||
stage.style.width = viewport.width + 'px';
|
||
const fabricWrap = stage.querySelector('.canvas-container');
|
||
if (fabricWrap) {
|
||
fabricWrap.style.width = viewport.width + 'px';
|
||
fabricWrap.style.height = viewport.height + 'px';
|
||
}
|
||
|
||
// Box aus aktuellen Werten
|
||
drawBoxFromInputs();
|
||
|
||
pageInfo.textContent = 'Seite ' + pageNum + ' / ' + pageCount + ' (' + pageWmm.toFixed(0) + ' × ' + pageHmm.toFixed(0) + ' mm)';
|
||
}
|
||
|
||
function drawBoxFromInputs() {
|
||
if (!fabricCanvas) return;
|
||
// ODT: keine Box zeichnen — Position kommt aus dem ODT-Bildplatzhalter
|
||
if (IS_ODT) {
|
||
fabricCanvas.clear();
|
||
fabricRect = null;
|
||
return;
|
||
}
|
||
// Wenn Seite nicht der Box-Seite entspricht: keine Box zeigen
|
||
if (pageNum !== chosenPageNum()) {
|
||
fabricCanvas.clear();
|
||
fabricRect = null;
|
||
return;
|
||
}
|
||
const xmm = parseFloat(inpX.value) || 0;
|
||
const ymm = parseFloat(inpY.value) || 0;
|
||
const wmm = parseFloat(inpW.value) || 50;
|
||
const hmm = parseFloat(inpH.value) || 25;
|
||
|
||
// mm → px = mm / PT_TO_MM * renderScale
|
||
const mm2px = (mm) => (mm / PT_TO_MM) * renderScale;
|
||
|
||
fabricCanvas.clear();
|
||
fabricRect = new fabric.Rect({
|
||
left: mm2px(xmm),
|
||
top: mm2px(ymm),
|
||
width: mm2px(wmm),
|
||
height: mm2px(hmm),
|
||
fill: 'rgba(60,140,220,0.20)',
|
||
stroke: '#1e6fc0',
|
||
strokeWidth: 2,
|
||
cornerColor: '#1e6fc0',
|
||
transparentCorners: false,
|
||
hasRotatingPoint: false,
|
||
lockRotation: true,
|
||
});
|
||
fabricCanvas.add(fabricRect);
|
||
fabricCanvas.setActiveObject(fabricRect);
|
||
|
||
fabricRect.on('modified', syncInputsFromRect);
|
||
fabricRect.on('moving', syncInputsFromRect);
|
||
fabricRect.on('scaling', syncInputsFromRect);
|
||
}
|
||
|
||
function syncInputsFromRect() {
|
||
if (!fabricRect) return;
|
||
const px2mm = (px) => (px / renderScale) * PT_TO_MM;
|
||
const wPx = fabricRect.width * (fabricRect.scaleX || 1);
|
||
const hPx = fabricRect.height * (fabricRect.scaleY || 1);
|
||
inpX.value = px2mm(fabricRect.left).toFixed(1);
|
||
inpY.value = px2mm(fabricRect.top).toFixed(1);
|
||
inpW.value = px2mm(wPx).toFixed(1);
|
||
inpH.value = px2mm(hPx).toFixed(1);
|
||
}
|
||
|
||
// Bei Number-Input-Aenderung Box neu zeichnen
|
||
[inpX, inpY, inpW, inpH].forEach(el => el.addEventListener('input', drawBoxFromInputs));
|
||
document.querySelector('select[name="page"]').addEventListener('change', () => {
|
||
pageNum = chosenPageNum();
|
||
renderPage();
|
||
});
|
||
|
||
document.getElementById('btn-prev-page').onclick = () => { if (pageNum > 1) { pageNum--; renderPage(); } };
|
||
document.getElementById('btn-next-page').onclick = () => { if (pageNum < pageCount) { pageNum++; renderPage(); } };
|
||
|
||
document.getElementById('btn-reset-box').onclick = () => {
|
||
inpX.value = DEFAULT_BOX.x_mm;
|
||
inpY.value = DEFAULT_BOX.y_mm;
|
||
inpW.value = DEFAULT_BOX.w_mm;
|
||
inpH.value = DEFAULT_BOX.h_mm;
|
||
document.querySelector('input[name="label"]').value = DEFAULT_BOX.label || 'Unterschrift Kunde';
|
||
document.querySelector('select[name="page"]').value = DEFAULT_BOX.page || 'last';
|
||
pageNum = chosenPageNum();
|
||
renderPage();
|
||
};
|
||
|
||
document.getElementById('btn-save-box').onclick = async () => {
|
||
const status = document.getElementById('save-status');
|
||
status.textContent = 'Speichere…';
|
||
const fd = new FormData(document.getElementById('box-form'));
|
||
fd.append('token', <?php echo json_encode(newToken()); ?>);
|
||
try {
|
||
const r = await fetch(SAVE_URL, { method: 'POST', body: fd, credentials: 'same-origin' });
|
||
const j = await r.json();
|
||
if (j.ok) status.textContent = '✓ Gespeichert um ' + new Date().toLocaleTimeString();
|
||
else status.textContent = '✕ ' + (j.error || 'Fehler');
|
||
} catch (e) {
|
||
status.textContent = '✕ ' + e.message;
|
||
}
|
||
};
|
||
|
||
loadPdf();
|
||
})();
|
||
</script>
|
||
<?php
|
||
}
|
||
|
||
llxFooter();
|
||
$db->close();
|