Files

443 lines
59 KiB
HTML

<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Printservice Standalone</title>
<style>
:root { font-family: Arial, Helvetica, sans-serif; color: #17202a; background: #f3f5f7; }
body { margin: 0; }
header { background: #1f2937; color: white; padding: 18px 24px; }
main { padding: 24px; display: grid; gap: 18px; max-width: 1480px; margin: auto; }
section { background: white; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,.08); padding: 18px; }
h1, h2, h3 { margin: 0 0 12px; }
h1 { font-size: 24px; } h2 { font-size: 18px; } h3 { font-size: 15px; margin-top: 16px; }
button { border: 0; border-radius: 8px; padding: 9px 13px; background: #2563eb; color: white; cursor: pointer; margin: 4px 4px 4px 0; }
button.secondary { background: #4b5563; } button.danger { background: #dc2626; } button.success { background: #059669; }
input, textarea, select { width: 100%; box-sizing: border-box; padding: 9px; border: 1px solid #cbd5e1; border-radius: 8px; margin: 4px 0 10px; font-family: inherit; }
textarea { min-height: 170px; font-family: Consolas, monospace; }
label { font-weight: 600; font-size: 13px; }
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
th, td { border-bottom: 1px solid #e5e7eb; padding: 9px; text-align: left; vertical-align: top; }
th { background: #f9fafb; }
pre { white-space: pre-wrap; background: #0f172a; color: #e5e7eb; padding: 12px; border-radius: 8px; overflow: auto; min-height: 80px; }
iframe { width: 100%; height: 360px; border: 1px solid #cbd5e1; border-radius: 8px; background: white; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(360px, 1fr)); gap: 18px; }
.small { color: #d1d5db; font-size: 13px; }
.muted { color: #6b7280; font-size: 13px; }
.status { border-left: 5px solid #2563eb; padding: 10px 12px; background: #eff6ff; border-radius: 8px; }
.hint { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 10px; font-size: 13px; color: #334155; }
.two { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.three { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; }
.designer { min-height: 260px; border: 1px solid #cbd5e1; border-radius: 8px; padding: 12px; background: white; overflow: auto; }
.badge { display: inline-block; background: #e5e7eb; border-radius: 999px; padding: 3px 8px; font-size: 12px; margin-right: 4px; }
.ok { border-left-color: #059669; background: #ecfdf5; }
.warn { border-left-color: #d97706; background: #fffbeb; }
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 12px; margin-top: 12px; }
.card { border: 1px solid #e2e8f0; border-radius: 10px; padding: 12px; background: #ffffff; }
.codebox { white-space: pre-wrap; background: #111827; color: #f9fafb; border-radius: 8px; padding: 10px; font-family: Consolas, monospace; font-size: 12px; min-height: 60px; }
.actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-top: 10px; }
.modal-backdrop { display:none; position:fixed; inset:0; background:rgba(15,23,42,.62); z-index:1000; padding:24px; overflow:auto; }
.modal-backdrop.open { display:block; }
.modal { background:white; max-width:980px; margin:30px auto; border-radius:14px; box-shadow:0 24px 60px rgba(0,0,0,.25); padding:20px; }
.modal header { background:transparent; color:#17202a; padding:0; display:flex; justify-content:space-between; gap:12px; align-items:center; }
.modal header h2 { margin:0; }
.close { background:#6b7280; }
.wide-table { display:block; overflow:auto; max-height:420px; border:1px solid #e5e7eb; border-radius:10px; }
@media (max-width: 900px) { .two, .three { grid-template-columns: 1fr; } }
</style>
</head>
<body>
<header>
<h1>Printservice Standalone</h1>
<div class="small">Eigenständige Java-25-Applikation · REST Druckservice · Drucker-Aliase · HTML→PDF mit Tabellen · PDF-Koordinatenvorlagen · DOCX-Vorlagen · ZPL/EPL-Rohdruck · Barcode · QR-Code · Ordnerbasierte Vorlagenspeicherung</div>
</header>
<main>
<section><h2>Status</h2><div id="status" class="status">Bereit.</div></section>
<section>
<h2>Authentifizierung, Benutzer und Token</h2>
<div class="hint">
Authentifizierung ist optional. Wenn sie deaktiviert ist, kann jeder, der den REST-Endpunkt erreicht, drucken. Wenn sie aktiviert wird, kann der Benutzer im Webfrontend angelegt werden und erhält ein Token. Das Token wird als Header <code>X-Printservice-Token</code> oder <code>Authorization: Bearer ...</code> gesendet.
</div>
<div class="grid">
<div>
<h3>Token im Browser nutzen</h3>
<label>Druck-Token</label><input id="apiToken" type="password" placeholder="Token für /api/print">
<button onclick="saveToken()">Token im Browser merken</button><button class="secondary" onclick="clearToken()">Token entfernen</button>
<button class="secondary" onclick="loadAuthStatus()">Status laden</button>
<label>Server-Auth-Status</label><pre id="authStatus">Noch nicht geladen.</pre>
</div>
<div>
<h3>Auth einschalten / ausschalten</h3>
<label><input id="authEnabled" type="checkbox" style="width:auto"> Authentifizierung aktivieren</label><br>
<label><input id="protectPrint" type="checkbox" style="width:auto" checked> Drucken schuetzen</label><br>
<label><input id="protectPreview" type="checkbox" style="width:auto"> Vorschau schuetzen</label><br>
<label><input id="protectAdmin" type="checkbox" style="width:auto"> Admin-Funktionen schuetzen</label><br>
<label>Header-Name</label><input id="authHeaderName" value="X-Printservice-Token">
<button onclick="saveAuthConfig()">Auth-Konfiguration speichern</button>
<div class="muted">Empfehlung: zuerst Admin-Benutzer anlegen, Token speichern, dann Auth aktivieren.</div>
</div>
<div>
<h3>Benutzer / Token anlegen</h3>
<label>User</label><input id="newUserName" placeholder="z.B. line01">
<label>Anzeigename</label><input id="newDisplayName" placeholder="Linie 01">
<label>Token-Name</label><input id="newTokenName" placeholder="line01-print-token">
<label>Scopes</label><select id="newScopes" multiple size="4"><option value="PRINT" selected>PRINT</option><option value="PREVIEW">PREVIEW</option><option value="ADMIN">ADMIN</option><option value="ALL">ALL</option></select>
<button onclick="createAuthUser()">User + Token erzeugen</button>
<button class="secondary" onclick="loadAuthUsers()">Benutzer laden</button>
</div>
</div>
<h3>Benutzerliste</h3>
<table><thead><tr><th>User</th><th>Name</th><th>Token</th><th>Scopes</th><th>Aktiv</th><th></th></tr></thead><tbody id="authUsersBody"></tbody></table>
<h3>Neues Token - nur einmal sichtbar</h3>
<pre id="newTokenResult">Noch kein Token erzeugt.</pre>
</section>
<section>
<h2>Speicherstruktur</h2>
<div class="hint">
Vorlagen werden produktionsnah als Ordner gespeichert: <code>templates/&lt;templateName&gt;/template.json</code>, <code>document.*</code>, <code>fields.json</code>, <code>sample-data.json</code> und <code>print-request.json</code>. Dadurch bleiben PDF/DOCX/HTML-Dateien auffindbar, sicherbar und separat versionierbar.
</div>
<button onclick="loadTemplateStorage()">Speicherpfade anzeigen</button>
<pre id="storageInfo">Noch nicht geladen.</pre>
</section>
<section>
<h2>1. Windows-/Server-Drucker-Assistent</h2>
<div class="hint">
Der Assistent erkennt, ob der Service unter Windows und wahrscheinlich unter <b>LocalSystem</b> laeuft. Die gefundene Druckerliste bleibt hier im Vordergrund. RAW_TCP und Windows-Kommandos sind in separate Dialoge ausgelagert, damit die Liste gut lesbar bleibt.
</div>
<div class="actions">
<button onclick="loadPrinterDiagnostics()">Assistent starten / Diagnose laden</button>
<button class="secondary" onclick="scanPrinters()">Spooler/CUPS-Drucker neu suchen</button>
<button class="success" onclick="openModal('rawTcpModal')">B. RAW_TCP fuer ZPL/EPL</button>
<button class="secondary" onclick="openModal('windowsCommandsModal')">C. Windows-Kommandos</button>
</div>
<div id="diagnosticsSummary" class="status" style="margin-top:10px">Noch keine Diagnose geladen.</div>
<h3>A. Gefundene Windows-Spooler-/CUPS-Drucker</h3>
<div class="muted">Geeignet fuer PDF, HTML-&gt;PDF, PDF_TEMPLATE und auch RAW-Druck ueber installierte Treiber. Die Liste ist absichtlich breit und scrollbar, damit lange Windows-Druckernamen lesbar bleiben.</div>
<div class="wide-table">
<table><thead><tr><th>Systemname</th><th>Standard</th><th>Aktion</th></tr></thead><tbody id="printerScanBody"></tbody></table>
</div>
</section>
<div id="rawTcpModal" class="modal-backdrop">
<div class="modal">
<header><h2>B. RAW_TCP fuer ZPL/EPL</h2><button class="close" onclick="closeModal('rawTcpModal')">Schliessen</button></header>
<div class="hint">Schnellster Weg fuer Etikettendrucker. RAW_TCP umgeht den Windows-Spooler und funktioniert auch dann, wenn der Service als LocalSystem laeuft. Geeignet fuer ZPL, EPL, ESC/POS, PCL oder reinen Text.</div>
<div class="three">
<div><label>Drucker-IP / Host</label><input id="rawHost" placeholder="192.168.10.50"></div>
<div><label>Port</label><input id="rawPort" type="number" min="1" max="65535" value="9100"></div>
<div><label>Druckersprache</label><select id="rawLanguage"><option value="ZPL">ZPL</option><option value="EPL">EPL</option><option value="ESC_POS">ESC/POS</option><option value="PCL">PCL</option><option value="TEXT">TEXT</option></select></div>
</div>
<button onclick="prepareRawTcpAlias(); closeModal('rawTcpModal')">RAW_TCP Alias vorbereiten</button>
</div>
</div>
<div id="windowsCommandsModal" class="modal-backdrop">
<div class="modal">
<header><h2>C. Windows-Kommandos</h2><button class="close" onclick="closeModal('windowsCommandsModal')">Schliessen</button></header>
<div class="hint">Diese Kommandos sind nur Hilfen fuer Administratoren. Sie sollten auf dem Server mit administrativen Rechten ausgefuehrt werden. Danach Spooler bzw. Dienst neu starten.</div>
<label>Lokaler TCP/IP-Drucker</label><div id="cmdLocalTcp" class="codebox">Noch nicht geladen.</div>
<label>Printserver maschinenweit</label><div id="cmdMachinePrinter" class="codebox">Noch nicht geladen.</div>
</div>
</div>
<section>
<h2>2. Eindeutigen Druckernamen zuweisen</h2>
<div class="three">
<div><label>Alias / eindeutiger Name</label><input id="printerAlias" placeholder="z.B. LABEL_LINE_01"></div>
<div><label>Druckertyp</label><select id="printerType" onchange="togglePrinterTypeFields()"><option value="WINDOWS_SPOOLER">WINDOWS_SPOOLER</option><option value="RAW_TCP">RAW_TCP</option></select></div>
<div><label>Beschreibung</label><input id="printerDescription" placeholder="z.B. Zebra an Linie 1"></div>
</div>
<div id="spoolerFields"><label>Systemdruckername</label><input id="systemName" placeholder="Name aus Windows Spooler oder Linux CUPS"></div>
<div id="rawTcpFields" class="three" style="display:none">
<div><label>Host/IP</label><input id="printerHost" placeholder="192.168.10.50"></div>
<div><label>Port</label><input id="printerPort" type="number" min="1" max="65535" value="9100"></div>
<div><label>Sprache</label><input id="printerLanguage" placeholder="ZPL, EPL, ESC_POS, PCL"></div>
</div>
<button onclick="savePrinterAlias()">Alias speichern</button><button class="secondary" onclick="loadPrinterAliases()">Aliase laden</button>
<table><thead><tr><th>Alias</th><th>Typ</th><th>Ziel</th><th>Aktiv</th><th></th></tr></thead><tbody id="printerAliasBody"></tbody></table>
</section>
<section>
<h2>3. Dokumentenvorlage einchecken oder online erstellen</h2>
<div class="hint">
Schnellster Druckpfad: <b>ZPL/EPL</b> direkt als Rohdaten oder <b>PDF_TEMPLATE</b> für fertige PDF-Vorlagen mit Koordinatenfeldern. HTML→PDF bleibt für Webvorlagen nutzbar und unterstützt Tabellen mit JSON-Wiederholungsblöcken wie <code>{{#each:items}}</code>.
PDF-Koordinatenfelder nutzen <code>x/y/width/height</code> in <code>pt</code> oder <code>mm</code>; Text, Barcode und QR werden direkt ins PDF gestempelt.
</div>
<div class="grid">
<div>
<label>Datei einchecken: .pdf, .html, .zpl, .epl, .txt, .xml, .docx</label>
<input id="templateFile" type="file" accept=".pdf,.html,.htm,.zpl,.epl,.txt,.tpl,.xml,.json,.docx">
<button class="success" onclick="checkInDocument()">Dokument einchecken</button>
<button class="secondary" onclick="insertHtmlSample()">HTML-Beispiel</button>
<button class="secondary" onclick="insertHtmlTableSample()">HTML-Tabellenbeispiel</button>
<button class="secondary" onclick="insertZplSample()">ZPL-Beispiel</button>
<button class="secondary" onclick="insertDocxInfo()">DOCX-Hinweis</button>
<button class="secondary" onclick="insertPdfFieldSample()">PDF-Feldbeispiel</button>
<button class="secondary" onclick="insertPdfTableSample()">PDF-Tabellenbeispiel</button>
</div>
<div>
<label>Vorlagenname / Dokumentname</label><input id="templateName" placeholder="z.B. warenanhaenger_html">
<label>Beschreibung</label><input id="templateDescription" placeholder="z.B. Warenanhänger mit Seriennummer und QR-Code">
<div id="binaryNotice" class="hint" style="display:none">Binärvorlage ist eingecheckt. DOCX/PDF wird als Datei im Vorlagenordner gespeichert. Für PDF werden die Koordinatenfelder separat in fields.json gepflegt.</div>
</div>
</div>
<div class="three">
<div><label>Content-Type</label><select id="templateContentType" onchange="syncTemplateTypeFromContentType()">
<option value="text/html">text/html - HTML-Dokument</option>
<option value="text/plain">text/plain - Text, ZPL, EPL, PCL</option>
<option value="application/xml">application/xml</option>
<option value="application/json">application/json</option>
<option value="application/vnd.openxmlformats-officedocument.wordprocessingml.document">application/vnd.openxmlformats-officedocument.wordprocessingml.document</option>
<option value="application/pdf">application/pdf - PDF-Koordinatenvorlage</option>
</select></div>
<div><label>Vorlagentyp</label><select id="templateType" onchange="toggleBinaryNotice()">
<option value="HTML">HTML</option><option value="PDF">PDF</option><option value="ZPL">ZPL</option><option value="EPL">EPL</option><option value="TEXT">TEXT</option><option value="XML">XML</option><option value="DOCX">DOCX</option>
</select></div>
<div><label>Encoding</label><input id="templateEncoding" value="UTF-8"></div>
</div>
<div class="two">
<div>
<label>Vorlageninhalt / Quelltext</label>
<textarea id="templateContent"></textarea>
<input id="templateContentBase64" type="hidden">
<button onclick="saveTemplate()">Vorlage speichern</button>
<button class="secondary" onclick="loadTemplates()">Vorlagen laden</button>
<button class="secondary" onclick="loadBarcodeFormats()">Barcode-Arten anzeigen</button>
<label>PDF-Koordinatenfelder als JSON</label>
<textarea id="pdfFieldsJson" placeholder='{"coordinateUnit":"mm","coordinateOrigin":"TOP_LEFT","pdfFields":[...]}'></textarea>
</div>
<div>
<label>Einfacher HTML-Designer</label>
<div id="htmlDesigner" class="designer" contenteditable="true"></div>
<button class="secondary" onclick="sourceToDesigner()">HTML aus Quelltext übernehmen</button>
<button class="secondary" onclick="designerToSource()">Designer nach Quelltext übernehmen</button>
<div class="muted">Der Designer ist für HTML-Vorlagen gedacht. DOCX bitte in Word oder LibreOffice bearbeiten und erneut einchecken.</div>
</div>
</div>
<h3>Eingecheckte Vorlagen</h3>
<table><thead><tr><th>Name</th><th>Dokument</th><th>Typ</th><th>Speicher</th><th>Größe</th><th>Geändert</th><th></th></tr></thead><tbody id="templateBody"></tbody></table>
<h3>Barcode-Arten</h3><div id="barcodeFormats" class="hint">Noch nicht geladen.</div>
</section>
<section>
<h2>4. JSON übergeben, Vorschau erzeugen und drucken</h2>
<div class="grid">
<div>
<label>Printer Alias</label><input id="printAlias" placeholder="LABEL_LINE_01">
<label>Template / Dokumentname</label><input id="printTemplate" placeholder="warenanhaenger_html">
<div class="three">
<div><label>Kopien</label><input id="copies" type="number" min="1" max="99" value="1"></div>
<div><label>Render-Modus</label><select id="renderMode"><option value="AUTO">AUTO</option><option value="PDF_TEMPLATE">PDF_TEMPLATE</option><option value="PDF">PDF</option><option value="RAW">RAW</option><option value="DOCX">DOCX</option></select></div>
<div><label>PDF-DPI</label><input id="pdfDpi" type="number" min="0" max="1200" value="203"></div>
</div>
<label>DOCX-Konverter</label><select id="docxConverter"><option value="NONE">NONE - nur DOCX herunterladen</option><option value="DOCX4J_FO">DOCX4J_FO - Java-only DOCX zu PDF</option><option value="LIBREOFFICE">LIBREOFFICE - DOCX zu PDF und drucken</option></select>
<label>JSON-Daten</label>
<textarea id="printData">{
"serialNumber": "SNR000001",
"partNumber": "A123456",
"workorderNumber": "WO-100200",
"deliveryNumber": "LS-2026-0001",
"deliveryDate": "08.06.2026",
"customer": { "name": "Muster GmbH" },
"items": [
{ "pos": "1", "partNumber": "A123456", "description": "Gehäuse Oberteil", "quantity": "10", "serialNumber": "SNR000001" },
{ "pos": "2", "partNumber": "B987654", "description": "Elektronikmodul", "quantity": "5", "serialNumber": "SNR000002" },
{ "pos": "3", "partNumber": "C555111", "description": "Dichtung", "quantity": "20", "serialNumber": "SNR000003" }
]
}</textarea>
<button onclick="previewPrint()">JSON-Vorschau</button>
<button onclick="previewHtmlOnly()">HTML-Vorschau</button>
<button onclick="downloadRendered()">Gerendertes Dokument herunterladen</button>
<button class="secondary" onclick="loadDocxConverters()">DOCX-Konverterstatus</button>
<button class="secondary" onclick="validateDocxConverter()">DOCX-Konverter validieren</button>
<button onclick="sendPrint()">Drucken</button>
</div>
<div>
<h3>Antwort / gerenderter Inhalt</h3><pre id="printResult">Noch kein Druckjob.</pre>
<h3>HTML-Vorschau</h3><iframe id="htmlPreview" title="HTML Vorschau"></iframe>
</div>
</div>
</section>
<section>
<h2>5. Druckwarteschlange / Druckhistorie, Datenschutz und Redruck</h2>
<div class="hint">
Die Queue ist eine eigene persistente Druckhistorie unter <code>queue/print-jobs.json</code>. Sie ist nicht die Windows-Spooler-Warteschlange. Aus Datenschutzgruenden kann sie vollstaendig deaktiviert werden. Wird der Originalrequest nicht gespeichert, bleiben nur Metadaten erhalten und Redruck ist fuer neue Jobs nicht moeglich.
</div>
<h3>Queue-Einstellungen</h3>
<div class="three">
<div><label><input id="queueEnabled" type="checkbox" style="width:auto" checked> Druckwarteschlange / Druckhistorie aktivieren</label><br><span class="muted">Aus = keine Job-Historie, kein Redruck, weniger personenbezogene Daten.</span></div>
<div><label><input id="queueStoreRequests" type="checkbox" style="width:auto" checked> Originalrequest fuer Redruck speichern</label><br><span class="muted">Aus = nur Metadaten, keine JSON-Druckdaten.</span></div>
<div><label><input id="queueAutoCleanup" type="checkbox" style="width:auto"> Automatisch loeschen aelter als X Tage</label></div>
</div>
<div class="three">
<div><label>Retention in Tagen</label><input id="queueRetentionDays" type="number" min="0" max="3650" value="30"></div>
<div><label>Maximal gespeicherte Jobs</label><input id="queueMaxStoredJobs" type="number" min="0" max="1000000" value="5000"></div>
<div class="actions"><button onclick="loadQueueSettings()">Einstellungen laden</button><button onclick="saveQueueSettings()">Einstellungen speichern</button></div>
</div>
<pre id="queueSettingsInfo">Noch nicht geladen.</pre>
<h3>Queue leeren / bereinigen</h3>
<div class="actions">
<button class="danger" onclick="clearQueue()">Druckwarteschlange komplett leeren</button>
<button class="secondary" onclick="cleanupQueueOlderThanDays()">Jobs aelter als X Tage loeschen</button>
</div>
<h3>Queue abrufen und Redruck</h3>
<div class="three">
<div><label>User-Filter</label><input id="queueUser" placeholder="leer = eigener User; Admin mit full=true sieht alle"></div>
<div><label>Status</label><select id="queueStatus"><option value="">Alle</option><option value="SUBMITTED">SUBMITTED</option><option value="FAILED">FAILED</option><option value="ACCEPTED">ACCEPTED</option></select></div>
<div><label>Limit</label><input id="queueLimit" type="number" min="1" max="1000" value="100"></div>
</div>
<div class="three">
<div><label>Printer Alias</label><input id="queuePrinterAlias" placeholder="optional"></div>
<div><label>Template</label><input id="queueTemplateName" placeholder="optional"></div>
<div><label><input id="queueFull" type="checkbox" style="width:auto"> Volle Liste abrufen (ADMIN)</label><br><label><input id="queueIncludeRequest" type="checkbox" style="width:auto"> Originalrequest anzeigen</label></div>
</div>
<button onclick="loadQueue()">Druckwarteschlange laden</button>
<div class="wide-table"><table><thead><tr><th>Zeit</th><th>Status</th><th>User</th><th>Drucker</th><th>Vorlage</th><th>Kopien</th><th>Job-ID</th><th></th></tr></thead><tbody id="queueBody"></tbody></table></div>
<h3>Job-Details</h3><pre id="queueDetails">Noch kein Job ausgewaehlt.</pre>
</section>
</main>
<script>
const appBase = window.location.pathname.endsWith('/') ? window.location.pathname : window.location.pathname.replace(/\/[^\/]*$/, '/');
const api = appBase.replace(/\/$/, '') + '/api';
function setStatus(text, isError = false) { const box = document.getElementById('status'); box.textContent = text; box.style.borderLeftColor = isError ? '#dc2626' : '#2563eb'; box.style.background = isError ? '#fef2f2' : '#eff6ff'; }
function openModal(id) { const el = document.getElementById(id); if (el) el.classList.add('open'); }
function closeModal(id) { const el = document.getElementById(id); if (el) el.classList.remove('open'); }
document.addEventListener('keydown', e => { if (e.key === 'Escape') document.querySelectorAll('.modal-backdrop.open').forEach(m => m.classList.remove('open')); });
function authHeaders(extra = {}) { const token = localStorage.getItem('printservice.token') || valOrEmpty('apiToken'); const headers = { ...(extra || {}) }; if (token) headers['X-Printservice-Token'] = token; return headers; }
async function request(path, options = {}) { const baseHeaders = { 'Content-Type': 'application/json', ...(options.headers || {}) }; const response = await fetch(api + path, { headers: authHeaders(baseHeaders), ...options }); if (!response.ok) { let message = response.status + ' ' + response.statusText; try { const err = await response.json(); message = err.message || message; } catch (ignore) {} throw new Error(message); } if (response.status === 204) return null; const contentType = response.headers.get('content-type') || ''; if (contentType.includes('application/json')) return response.json(); return response.text(); }
function saveToken() { localStorage.setItem('printservice.token', val('apiToken')); setStatus('Token lokal im Browser gespeichert.'); }
function clearToken() { localStorage.removeItem('printservice.token'); document.getElementById('apiToken').value = ''; setStatus('Token entfernt.'); }
async function loadAuthStatus() { try { const info = await request('/auth/status'); document.getElementById('authStatus').textContent = JSON.stringify(info, null, 2); document.getElementById('authEnabled').checked = !!info.enabled; document.getElementById('protectPrint').checked = !!info.protectPrint; document.getElementById('protectPreview').checked = !!info.protectPreview; document.getElementById('protectAdmin').checked = !!info.protectAdmin; document.getElementById('authHeaderName').value = info.headerName || 'X-Printservice-Token'; await loadAuthUsers(); } catch (e) { document.getElementById('authStatus').textContent = e.message; } }
async function saveAuthConfig() { try { const payload = { enabled: document.getElementById('authEnabled').checked, protectPrint: document.getElementById('protectPrint').checked, protectPreview: document.getElementById('protectPreview').checked, protectAdmin: document.getElementById('protectAdmin').checked, headerName: val('authHeaderName') || 'X-Printservice-Token' }; const info = await request('/auth/config', { method:'PUT', body: JSON.stringify(payload) }); document.getElementById('authStatus').textContent = JSON.stringify(info, null, 2); setStatus(info.openPrintWithoutToken ? 'Auth gespeichert: Drucken ist weiterhin offen.' : 'Auth gespeichert: Drucken ist jetzt token-geschützt.'); } catch(e) { setStatus(e.message, true); } }
function selectedScopes() { return Array.from(document.getElementById('newScopes').selectedOptions).map(o => o.value); }
async function createAuthUser() { try { const payload = { userName: val('newUserName'), displayName: val('newDisplayName'), tokenName: val('newTokenName'), scopes: selectedScopes(), enabled: true }; const created = await request('/auth/users', { method:'POST', body: JSON.stringify(payload) }); document.getElementById('newTokenResult').textContent = JSON.stringify(created, null, 2); if (created.token) { document.getElementById('apiToken').value = created.token; localStorage.setItem('printservice.token', created.token); } await loadAuthUsers(); await loadAuthStatus(); setStatus('Benutzer/Token erzeugt. Token wurde lokal im Browser gesetzt.'); } catch(e) { setStatus(e.message, true); } }
async function loadAuthUsers() { try { const users = await request('/auth/users'); const body = document.getElementById('authUsersBody'); if (!body) return; body.innerHTML = ''; users.forEach(u => { const tr = document.createElement('tr'); tr.innerHTML = `<td>${escapeHtml(u.userName)}</td><td>${escapeHtml(u.displayName || '')}</td><td>${escapeHtml(u.tokenName || '')}</td><td>${escapeHtml((u.scopes || []).join(', '))}</td><td>${u.enabled ? 'Ja' : 'Nein'}</td>`; const td = document.createElement('td'); const del = document.createElement('button'); del.textContent = 'Löschen'; del.className = 'danger'; del.onclick = async () => { await request('/auth/users/' + encodeURIComponent(u.id || u.userName), { method:'DELETE' }); await loadAuthUsers(); await loadAuthStatus(); }; td.appendChild(del); tr.appendChild(td); body.appendChild(tr); }); } catch(e) { const body = document.getElementById('authUsersBody'); if (body) body.innerHTML = `<tr><td colspan="6">${escapeHtml(e.message)}</td></tr>`; } }
async function loadTemplateStorage() { try { const info = await request('/templates/storage'); document.getElementById('storageInfo').textContent = JSON.stringify(info, null, 2); setStatus('Speicherstruktur geladen (Ordnerstruktur ohne Legacy): ' + info.templatesDir); } catch (e) { setStatus(e.message, true); } }
async function loadPrinterDiagnostics() { try { setStatus('Lade Windows-/Druckerdiagnose ...'); const d = await request('/printers/diagnostics'); const summary = document.getElementById('diagnosticsSummary'); const warnings = (d.warnings || []).map(escapeHtml).join('<br>'); const recs = (d.recommendations || []).map(r => '• ' + escapeHtml(r)).join('<br>'); summary.className = 'status ' + ((d.warnings || []).length ? 'warn' : 'ok'); summary.innerHTML = `<b>OS:</b> ${escapeHtml(d.osName)} · <b>Java-User:</b> ${escapeHtml(d.javaUser)} · <b>USERNAME:</b> ${escapeHtml(d.userNameEnv)} · <b>Drucker:</b> ${d.discoveredPrinterCount}<br>${warnings ? '<b>Warnungen:</b><br>' + warnings + '<br>' : ''}<b>Empfehlung:</b><br>${recs || 'Keine besondere Massnahme erforderlich.'}`; document.getElementById('cmdLocalTcp').textContent = (d.commands || {}).localTcpPrinterPowerShell || ''; document.getElementById('cmdMachinePrinter').textContent = (d.commands || {}).perMachineSharedPrinterCmd || ''; await scanPrinters(false); setStatus('Diagnose geladen. Waehle nun WINDOWS_SPOOLER oder RAW_TCP.'); } catch (e) { setStatus(e.message, true); } }
async function scanPrinters(showStatus = true) { try { if (showStatus) setStatus('Suche Drucker ...'); const printers = await request('/printers/scan'); const body = document.getElementById('printerScanBody'); body.innerHTML = ''; printers.forEach(p => { const tr = document.createElement('tr'); tr.innerHTML = `<td>${escapeHtml(p.systemName)}</td><td>${p.defaultPrinter ? 'Ja' : 'Nein'}</td>`; const td = document.createElement('td'); const btn = document.createElement('button'); btn.textContent = 'Als WINDOWS_SPOOLER nutzen'; btn.onclick = () => { document.getElementById('printerType').value = 'WINDOWS_SPOOLER'; togglePrinterTypeFields(); document.getElementById('systemName').value = p.systemName; document.getElementById('printerAlias').value = p.systemName.replace(/[^A-Za-z0-9._-]/g, '_').toUpperCase(); document.getElementById('printAlias').value = document.getElementById('printerAlias').value; }; td.appendChild(btn); tr.appendChild(td); body.appendChild(tr); }); if (showStatus) setStatus(printers.length + ' Drucker gefunden.'); } catch (e) { setStatus(e.message, true); } }
function togglePrinterTypeFields() { const type = val('printerType') || 'WINDOWS_SPOOLER'; document.getElementById('spoolerFields').style.display = type === 'WINDOWS_SPOOLER' ? '' : 'none'; document.getElementById('rawTcpFields').style.display = type === 'RAW_TCP' ? '' : 'none'; }
function prepareRawTcpAlias() { document.getElementById('printerType').value = 'RAW_TCP'; togglePrinterTypeFields(); document.getElementById('printerHost').value = val('rawHost'); document.getElementById('printerPort').value = val('rawPort') || '9100'; document.getElementById('printerLanguage').value = val('rawLanguage'); if (!val('printerAlias')) document.getElementById('printerAlias').value = 'LABEL_LINE_01'; if (!val('printerDescription')) document.getElementById('printerDescription').value = 'Direkter RAW_TCP Etikettendrucker'; document.getElementById('printAlias').value = val('printerAlias'); setStatus('RAW_TCP Alias vorbereitet. Pruefe Alias/Host/Port und speichere den Drucker.'); }
async function savePrinterAlias() { try { const type = val('printerType') || 'WINDOWS_SPOOLER'; const payload = { alias: val('printerAlias'), type, description: val('printerDescription'), enabled: true }; if (type === 'RAW_TCP') { payload.host = val('printerHost'); payload.port = Number(val('printerPort') || 9100); payload.language = val('printerLanguage') || val('rawLanguage'); } else { payload.systemName = val('systemName'); } const saved = await request('/printers', { method: 'POST', body: JSON.stringify(payload) }); setStatus('Alias gespeichert: ' + saved.alias + ' (' + (saved.type || 'WINDOWS_SPOOLER') + ')'); await loadPrinterAliases(); } catch (e) { setStatus(e.message, true); } }
async function loadPrinterAliases() { try { const mappings = await request('/printers'); const body = document.getElementById('printerAliasBody'); body.innerHTML = ''; mappings.forEach(m => { const type = m.type || 'WINDOWS_SPOOLER'; const target = type === 'RAW_TCP' ? `${escapeHtml(m.host || '')}:${escapeHtml(String(m.port || 9100))} ${escapeHtml(m.language || '')}` : escapeHtml(m.systemName || ''); const tr = document.createElement('tr'); tr.innerHTML = `<td>${escapeHtml(m.alias)}</td><td>${escapeHtml(type)}</td><td>${target}</td><td>${m.enabled ? 'Ja' : 'Nein'}</td>`; const td = document.createElement('td'); const use = document.createElement('button'); use.textContent = 'Nutzen'; use.onclick = () => document.getElementById('printAlias').value = m.alias; const edit = document.createElement('button'); edit.textContent = 'Bearbeiten'; edit.className = 'secondary'; edit.onclick = () => { document.getElementById('printerAlias').value = m.alias || ''; document.getElementById('printerType').value = type; togglePrinterTypeFields(); document.getElementById('systemName').value = m.systemName || ''; document.getElementById('printerHost').value = m.host || ''; document.getElementById('printerPort').value = m.port || 9100; document.getElementById('printerLanguage').value = m.language || ''; document.getElementById('printerDescription').value = m.description || ''; }; const del = document.createElement('button'); del.textContent = 'Löschen'; del.className = 'danger'; del.onclick = async () => { await request('/printers/' + encodeURIComponent(m.alias), { method: 'DELETE' }); await loadPrinterAliases(); }; td.appendChild(use); td.appendChild(edit); td.appendChild(del); tr.appendChild(td); body.appendChild(tr); }); } catch (e) { setStatus(e.message, true); } }
async function checkInDocument() { try { const file = document.getElementById('templateFile').files[0]; if (!file) throw new Error('Bitte zuerst eine Datei auswählen.'); const name = stripExtension(file.name).replace(/[^A-Za-z0-9._-]/g, '_'); document.getElementById('templateName').value = val('templateName') || name; document.getElementById('templateContentType').value = inferContentType(file.name); document.getElementById('templateType').value = inferTemplateType(file.name, val('templateContentType')); if (file.name.toLowerCase().endsWith('.docx') || file.name.toLowerCase().endsWith('.pdf')) { document.getElementById('templateContentBase64').value = await fileToBase64(file); document.getElementById('templateContent').value = (file.name.toLowerCase().endsWith('.pdf') ? 'PDF-Datei eingecheckt: ' : 'DOCX-Datei eingecheckt: ') + file.name + '\nBei PDF bitte die Koordinatenfelder im JSON-Bereich definieren.'; if (file.name.toLowerCase().endsWith('.pdf')) insertPdfFieldSample(false); } else { document.getElementById('templateContent').value = await file.text(); document.getElementById('templateContentBase64').value = ''; if (val('templateType') === 'HTML') sourceToDesigner(); } toggleBinaryNotice(); await saveTemplate(file.name); } catch (e) { setStatus(e.message, true); } }
async function saveTemplate(sourceFileName) { try { const isBinary = val('templateType') === 'DOCX' || val('templateType') === 'PDF' || val('templateType') === 'PDF_TEMPLATE'; const pdfCfg = parsePdfFieldsJson(); let sampleData = null; try { sampleData = JSON.parse(val('printData') || '{}'); } catch(ignore) {} const payload = { name: val('templateName'), documentName: val('templateName'), sourceFileName: sourceFileName || (document.getElementById('templateFile').files[0] || {}).name || '', description: val('templateDescription'), encoding: val('templateEncoding'), content: isBinary ? null : val('templateContent'), contentBase64: isBinary ? val('templateContentBase64') : null, contentType: val('templateContentType'), templateType: val('templateType'), coordinateUnit: pdfCfg.coordinateUnit || 'pt', coordinateOrigin: pdfCfg.coordinateOrigin || 'BOTTOM_LEFT', pdfFields: pdfCfg.pdfFields || [], sampleData: sampleData, examplePrintRequest: { printerAlias: val('printAlias') || 'OFFICE_A4', templateName: val('templateName'), renderMode: val('renderMode') || 'AUTO', copies: Number(val('copies') || 1), pdfDpi: Number(val('pdfDpi') || 203), data: sampleData || {} } }; const saved = await request('/templates', { method: 'POST', body: JSON.stringify(payload) }); document.getElementById('printTemplate').value = saved.name; setStatus('Vorlage im Ordner gespeichert: ' + (saved.templateDirectory || saved.name)); await loadTemplates(); await loadTemplateStorage(); } catch (e) { setStatus(e.message, true); } }
async function loadTemplates() { try { const templates = await request('/templates'); const body = document.getElementById('templateBody'); body.innerHTML = ''; templates.forEach(t => { const storage = (t.storageMode || '') + (t.templateDirectory ? '<br><span class="muted">' + escapeHtml(t.templateDirectory) + '/' + escapeHtml(t.documentFile || 'document.*') + '</span>' : ''); const tr = document.createElement('tr'); tr.innerHTML = `<td>${escapeHtml(t.name)}</td><td>${escapeHtml(t.sourceFileName || t.documentName || '')}</td><td>${escapeHtml(t.templateType || '')}</td><td>${storage}</td><td>${formatBytes(t.sizeBytes)}</td><td>${escapeHtml(t.updatedAt || '')}</td>`; const td = document.createElement('td'); const edit = document.createElement('button'); edit.textContent = 'Bearbeiten'; edit.onclick = async () => { const full = await request('/templates/' + encodeURIComponent(t.name)); document.getElementById('templateName').value = full.name; document.getElementById('templateDescription').value = full.description || ''; document.getElementById('templateEncoding').value = full.encoding || 'UTF-8'; document.getElementById('templateContent').value = full.content || (((full.templateType === 'DOCX') || (full.templateType === 'PDF') || (full.templateType === 'PDF_TEMPLATE')) ? full.templateType + '-Datei geladen: ' + (full.documentFile || full.sourceFileName || full.documentName || full.name) : ''); document.getElementById('templateContentBase64').value = full.contentBase64 || ''; document.getElementById('pdfFieldsJson').value = JSON.stringify({coordinateUnit: full.coordinateUnit || 'pt', coordinateOrigin: full.coordinateOrigin || 'BOTTOM_LEFT', pdfFields: full.pdfFields || []}, null, 2); document.getElementById('templateContentType').value = full.contentType || 'text/plain'; document.getElementById('templateType').value = full.templateType || 'TEXT'; document.getElementById('printTemplate').value = full.name; if (full.sampleData) document.getElementById('printData').value = JSON.stringify(full.sampleData, null, 2); toggleBinaryNotice(); if ((full.templateType || '') === 'HTML') sourceToDesigner(); setStatus('Vorlage geladen aus ' + (full.templateDirectory || full.storageMode || 'Speicher') + ': ' + full.name); }; const use = document.createElement('button'); use.textContent = 'Nutzen'; use.onclick = () => document.getElementById('printTemplate').value = t.name; const del = document.createElement('button'); del.textContent = 'Löschen'; del.className = 'danger'; del.onclick = async () => { await request('/templates/' + encodeURIComponent(t.name), { method: 'DELETE' }); await loadTemplates(); }; td.appendChild(edit); td.appendChild(use); td.appendChild(del); tr.appendChild(td); body.appendChild(tr); }); } catch (e) { setStatus(e.message, true); } }
async function previewPrint() { try { const payload = printPayload(true); payload.includeRenderedBase64 = false; const result = await request('/print/preview', { method: 'POST', body: JSON.stringify(payload) }); document.getElementById('printResult').textContent = JSON.stringify(result, null, 2); if (result.renderedContent && (result.renderedContent.trim().startsWith('<') || (result.contentType || '').includes('html'))) document.getElementById('htmlPreview').srcdoc = result.renderedContent; setStatus('Vorschau erstellt. Render-Modus: ' + result.renderMode); } catch (e) { setStatus(e.message, true); } }
async function previewHtmlOnly() { try { const payload = printPayload(true); payload.renderMode = 'RAW'; const result = await request('/print/preview/html', { method: 'POST', body: JSON.stringify(payload) }); document.getElementById('htmlPreview').srcdoc = result; document.getElementById('printResult').textContent = result; setStatus('HTML-Vorschau erstellt.'); } catch (e) { setStatus(e.message, true); } }
async function sendPrint() { try { const result = await request('/print', { method: 'POST', body: JSON.stringify(printPayload(false)) }); document.getElementById('printResult').textContent = JSON.stringify(result, null, 2); setStatus('Druckjob gesendet: ' + result.jobId); await loadQueue(); } catch (e) { setStatus(e.message, true); await loadQueue().catch(()=>{}); } }
async function loadDocxConverters() { try { const info = await request('/docx/converters'); document.getElementById('printResult').textContent = JSON.stringify(info, null, 2); setStatus('DOCX-Konverterstatus geladen.'); } catch(e) { setStatus(e.message, true); } }
async function validateDocxConverter() { try { const converter = val('docxConverter') || 'DOCX4J_FO'; const payload = printPayload(true); payload.renderMode = 'DOCX'; payload.docxConverter = converter; payload.includeRenderedBase64 = false; const result = await request('/docx/validate?converter=' + encodeURIComponent(converter), { method: 'POST', body: JSON.stringify(payload) }); document.getElementById('printResult').textContent = JSON.stringify(result, null, 2); setStatus('DOCX-Konverter validiert: ' + result.converter + ', PDF Bytes: ' + result.pdfBytes + ', Zeit: ' + result.elapsedMs + ' ms'); } catch(e) { setStatus(e.message, true); } }
async function downloadRendered() { try { const response = await fetch(api + '/print/rendered', { method: 'POST', headers: authHeaders({'Content-Type':'application/json'}), body: JSON.stringify(printPayload(true)) }); if (!response.ok) throw new Error(await response.text()); const blob = await response.blob(); const cd = response.headers.get('content-disposition') || ''; const m = cd.match(/filename="?([^";]+)"?/i); const name = m ? m[1] : 'printdocument'; const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = name; a.click(); URL.revokeObjectURL(a.href); setStatus('Dokument heruntergeladen: ' + name); } catch (e) { setStatus(e.message, true); } }
function printPayload(dryRun) { return { printerAlias: val('printAlias'), templateName: val('printTemplate'), copies: Number(val('copies') || 1), dryRun, renderMode: val('renderMode'), pdfDpi: Number(val('pdfDpi') || 203), docxConverter: val('docxConverter'), data: JSON.parse(val('printData')) }; }
function insertHtmlSample() { document.getElementById('templateContentType').value = 'text/html'; document.getElementById('templateType').value = 'HTML'; document.getElementById('templateName').value = val('templateName') || 'warenanhaenger_html'; document.getElementById('templateContentBase64').value = ''; document.getElementById('templateContent').value = `<!doctype html>
<html>
<head><meta charset="utf-8"><title>Warenanhänger</title>
<style>@page { size: 90mm 130mm; margin: 6mm; } body { font-family: Arial, sans-serif; font-size: 12pt; } .label { width: 78mm; }</style></head>
<body><div class="label">
<h2>Warenanhänger</h2>
<p><b>Serial:</b> {{text:serialNumber}}</p>
<p><b>Part:</b> {{text:partNumber}}</p>
<p><b>Order:</b> {{text:workorderNumber}}</p>
<div><img alt="Barcode" src="{{barcode:CODE_128:serialNumber:360x90}}"></div>
<div><img alt="QR" src="{{qr:serialNumber}}" style="width:120px;height:120px;"></div>
</div></body></html>`; toggleBinaryNotice(); sourceToDesigner(); }
function insertHtmlTableSample() { document.getElementById('templateContentType').value = 'text/html'; document.getElementById('templateType').value = 'HTML'; document.getElementById('templateName').value = val('templateName') || 'lieferschein_html_table'; document.getElementById('templateContentBase64').value = ''; document.getElementById('templateContent').value = `<!doctype html>
<html>
<head><meta charset="utf-8"><title>Lieferschein HTML Tabelle</title>
<style>
@page { size: A4; margin: 14mm; }
body { font-family: Arial, sans-serif; font-size: 10pt; color: #111; }
h1 { font-size: 18pt; margin: 0 0 8mm 0; }
.header { display: flex; justify-content: space-between; align-items: flex-start; }
.box { border: 1px solid #999; padding: 3mm; margin-bottom: 5mm; }
table { width: 100%; border-collapse: collapse; table-layout: fixed; }
th, td { border: 1px solid #777; padding: 2mm; vertical-align: top; }
th { background: #eeeeee; font-weight: bold; }
.right { text-align: right; } .center { text-align: center; }
.barcode { width: 42mm; height: 12mm; object-fit: contain; }
.qr { width: 28mm; height: 28mm; }
</style></head>
<body>
<div class="header">
<div><h1>Lieferschein</h1><div><b>Nr.:</b> {{text:deliveryNumber}}</div><div><b>Datum:</b> {{text:deliveryDate}}</div></div>
<img class="qr" alt="QR" src="{{qr:deliveryNumber}}">
</div>
<div class="box"><b>Kunde:</b> {{text:customer.name}}</div>
<table>
<thead><tr><th style="width:10mm">Pos.</th><th style="width:32mm">Artikel</th><th>Bezeichnung</th><th style="width:18mm">Menge</th><th style="width:38mm">Seriennummer</th><th style="width:45mm">Barcode</th></tr></thead>
<tbody>
{{#each:items}}
<tr>
<td class="center">{{text:pos}}</td>
<td>{{text:partNumber}}</td>
<td>{{text:description}}</td>
<td class="right">{{text:quantity}}</td>
<td>{{text:serialNumber}}</td>
<td class="center"><img class="barcode" alt="Barcode" src="{{barcode:CODE_128:serialNumber:360x90}}"></td>
</tr>
{{/each}}
</tbody>
</table>
</body>
</html>`; document.getElementById('renderMode').value = 'PDF'; toggleBinaryNotice(); sourceToDesigner(); }
function insertZplSample() { document.getElementById('templateContentType').value = 'text/plain'; document.getElementById('templateType').value = 'ZPL'; document.getElementById('templateName').value = val('templateName') || 'serial_label_zpl'; document.getElementById('templateContentBase64').value = ''; document.getElementById('templateContent').value = `^XA
^CF0,35
^FO40,40^FDSerial: {{serialNumber}}^FS
^FO40,90^FDPart: {{partNumber}}^FS
^FO40,145^BCN,80,Y,N,N^FD{{serialNumber}}^FS
^FO40,260^BQN,2,6^FDLA,{{serialNumber}}^FS
^XZ`; toggleBinaryNotice(); }
function insertDocxInfo() { document.getElementById('templateContentType').value = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; document.getElementById('templateType').value = 'DOCX'; document.getElementById('templateContent').value = 'DOCX-Vorlage in Word/LibreOffice erstellen. Platzhalter im Dokument verwenden:\n{{serialNumber}}\n{{text:partNumber}}\n{{barcode:CODE_128:serialNumber:360x90}} als alleiniger Absatz\n{{qr:serialNumber}} als alleiniger Absatz'; toggleBinaryNotice(); }
function insertPdfFieldSample(setName = true) { if (setName) { document.getElementById('templateContentType').value = 'application/pdf'; document.getElementById('templateType').value = 'PDF'; document.getElementById('templateName').value = val('templateName') || 'lieferschein_pdf'; } document.getElementById('pdfFieldsJson').value = JSON.stringify({ coordinateUnit: 'mm', coordinateOrigin: 'TOP_LEFT', pdfFields: [ { name:'serialNumber', type:'TEXT', value:'{{serialNumber}}', page:1, x:35, y:45, width:60, height:6, fontSize:10 }, { name:'partNumber', type:'TEXT', value:'{{partNumber}}', page:1, x:35, y:53, width:60, height:6, fontSize:10 }, { name:'workorderNumber', type:'TEXT', value:'{{workorderNumber}}', page:1, x:35, y:61, width:60, height:6, fontSize:10 }, { name:'serialBarcode', type:'BARCODE', barcodeType:'CODE_128', value:'{{serialNumber}}', page:1, x:25, y:84, width:90, height:18 }, { name:'serialQr', type:'QR_CODE', value:'{{serialNumber}}', page:1, x:155, y:38, width:32, height:32 } ] }, null, 2); toggleBinaryNotice(); }
function insertPdfTableSample() { document.getElementById('templateContentType').value = 'application/pdf'; document.getElementById('templateType').value = 'PDF'; document.getElementById('templateName').value = val('templateName') || 'lieferschein_pdf_table'; document.getElementById('pdfFieldsJson').value = JSON.stringify({ coordinateUnit: 'mm', coordinateOrigin: 'TOP_LEFT', pdfFields: [ { name:'deliveryNumber', type:'TEXT', value:'{{deliveryNumber}}', page:1, x:38, y:35, width:70, height:6, fontSize:10 }, { name:'customerName', type:'TEXT', value:'{{customer.name}}', page:1, x:38, y:43, width:90, height:6, fontSize:10 }, { name:'serialQr', type:'QR_CODE', value:'{{deliveryNumber}}', page:1, x:162, y:25, width:28, height:28 }, { name:'positionsTable', type:'TABLE', source:'items', page:1, x:18, y:78, width:174, height:125, rowHeight:13, headerHeight:8, maxRows:9, overflow:'CUT', fontSize:7, border:true, headerBackground:'#EEEEEE', columns:[ { title:'Pos.', field:'pos', width:12, align:'CENTER' }, { title:'Artikel', field:'partNumber', width:29 }, { title:'Bezeichnung', field:'description', width:47, multiline:true }, { title:'Menge', field:'quantity', width:16, align:'RIGHT' }, { title:'SNR', field:'serialNumber', width:31 }, { title:'Barcode', type:'BARCODE', barcodeType:'CODE_128', field:'serialNumber', width:39 } ] } ] }, null, 2); toggleBinaryNotice(); }
function placeholderSvg(label) {
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="260" height="70"><rect x="1" y="1" width="258" height="68" rx="6" fill="#f8fafc" stroke="#64748b" stroke-dasharray="6 4"/><text x="130" y="39" text-anchor="middle" font-family="Arial" font-size="14" fill="#334155">${label}</text></svg>`;
return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg);
}
function htmlForDesigner(html) {
// Prevent the browser from requesting literal {{barcode:...}} or {{qr:...}} URLs while editing.
return String(html || '').replace(/src=(['"])(\{\{\s*(barcode|qr|data_matrix|datamatrix)[^'"]*\}\})\1/gi,
function(match, quote, token, kind) {
const label = kind.toLowerCase().startsWith('qr') ? 'QR-Platzhalter' : 'Barcode-Platzhalter';
return 'src="' + placeholderSvg(label) + '" data-template-src="' + escapeHtml(token) + '"';
});
}
function htmlFromDesigner(html) {
return String(html || '').replace(/src="data:image\/svg\+xml[^\"]*"\s+data-template-src="([^"]+)"/gi,
function(match, token) { return 'src="' + token.replace(/&quot;/g, '"').replace(/&amp;/g, '&') + '"'; });
}
function sourceToDesigner() { document.getElementById('htmlDesigner').innerHTML = htmlForDesigner(val('templateContent')); }
function designerToSource() { document.getElementById('templateContent').value = htmlFromDesigner(document.getElementById('htmlDesigner').innerHTML); }
async function loadQueueSettings() { try { const s = await request('/queue/settings'); document.getElementById('queueEnabled').checked = !!s.enabled; document.getElementById('queueStoreRequests').checked = !!s.storeRequestsForReprint; document.getElementById('queueAutoCleanup').checked = !!s.autoCleanupEnabled; document.getElementById('queueRetentionDays').value = s.retentionDays ?? 30; document.getElementById('queueMaxStoredJobs').value = s.maxStoredJobs ?? 5000; document.getElementById('queueSettingsInfo').textContent = JSON.stringify(s, null, 2); setStatus('Queue-Einstellungen geladen.'); } catch(e) { document.getElementById('queueSettingsInfo').textContent = e.message; } }
async function saveQueueSettings() { try { const payload = { enabled: document.getElementById('queueEnabled').checked, storeRequestsForReprint: document.getElementById('queueStoreRequests').checked, autoCleanupEnabled: document.getElementById('queueAutoCleanup').checked, retentionDays: Number(val('queueRetentionDays') || 0), maxStoredJobs: Number(val('queueMaxStoredJobs') || 0) }; const s = await request('/queue/settings', { method:'PUT', body: JSON.stringify(payload) }); document.getElementById('queueSettingsInfo').textContent = JSON.stringify(s, null, 2); setStatus(s.enabled ? 'Queue-Einstellungen gespeichert.' : 'Queue deaktiviert: neue Druckjobs werden nicht gespeichert.'); await loadQueue(); } catch(e) { setStatus(e.message, true); } }
async function clearQueue() { try { if (!confirm('Druckwarteschlange wirklich komplett leeren? Redruck der gelöschten Jobs ist danach nicht mehr möglich.')) return; const result = await request('/queue', { method:'DELETE' }); document.getElementById('queueDetails').textContent = JSON.stringify(result, null, 2); setStatus('Queue geleert: ' + result.removed + ' Jobs entfernt.'); await loadQueue(); } catch(e) { setStatus(e.message, true); } }
async function cleanupQueueOlderThanDays() { try { const days = Number(val('queueRetentionDays') || 0); if (!confirm('Jobs älter als ' + days + ' Tage löschen?')) return; const result = await request('/queue/cleanup?olderThanDays=' + encodeURIComponent(days), { method:'POST', body:'{}' }); document.getElementById('queueDetails').textContent = JSON.stringify(result, null, 2); setStatus('Queue bereinigt: ' + result.removed + ' Jobs entfernt.'); await loadQueue(); } catch(e) { setStatus(e.message, true); } }
async function loadQueue() { try { const params = new URLSearchParams(); if (val('queueUser')) params.set('user', val('queueUser')); if (val('queueStatus')) params.set('status', val('queueStatus')); if (val('queuePrinterAlias')) params.set('printerAlias', val('queuePrinterAlias')); if (val('queueTemplateName')) params.set('templateName', val('queueTemplateName')); if (val('queueLimit')) params.set('limit', val('queueLimit')); if (document.getElementById('queueFull').checked) params.set('full','true'); if (document.getElementById('queueIncludeRequest').checked) params.set('includeRequest','true'); const q = await request('/queue?' + params.toString(), { headers: {} }); const body = document.getElementById('queueBody'); body.innerHTML = ''; (q.jobs || []).forEach(job => { const tr = document.createElement('tr'); tr.innerHTML = `<td>${escapeHtml(job.createdAt || '')}</td><td>${escapeHtml(job.status || '')}</td><td>${escapeHtml(job.userName || '')}</td><td>${escapeHtml(job.printerAlias || '')}<br><span class="muted">${escapeHtml(job.systemName || '')}</span></td><td>${escapeHtml(job.templateName || '')}</td><td>${escapeHtml(String(job.copies || ''))}</td><td><span class="muted">${escapeHtml(job.jobId || '')}</span></td>`; const td = document.createElement('td'); const details = document.createElement('button'); details.textContent = 'Details'; details.onclick = () => showQueueJob(job.jobId); td.appendChild(details); const reprint = document.createElement('button'); reprint.textContent = q.storeRequestsForReprint ? 'Redruck' : 'Redruck aus'; reprint.className = q.storeRequestsForReprint ? 'success' : 'secondary'; reprint.disabled = !q.storeRequestsForReprint; reprint.onclick = () => reprintJob(job.jobId); td.appendChild(reprint); tr.appendChild(td); body.appendChild(tr); }); document.getElementById('queueDetails').textContent = JSON.stringify(q, null, 2); setStatus((q.message || 'Druckwarteschlange geladen') + ' - ' + q.returned + ' von ' + q.total); } catch(e) { setStatus(e.message, true); } }
async function showQueueJob(jobId) { try { const job = await request('/queue/' + encodeURIComponent(jobId)); document.getElementById('queueDetails').textContent = JSON.stringify(job, null, 2); } catch(e) { setStatus(e.message, true); } }
async function reprintJob(jobId) { try { const payload = {}; const c = prompt('Kopien für Redruck leer lassen = Original verwenden:', ''); if (c && c.trim()) payload.copies = Number(c); const result = await request('/queue/' + encodeURIComponent(jobId) + '/reprint', { method:'POST', body: JSON.stringify(payload) }); document.getElementById('printResult').textContent = JSON.stringify(result, null, 2); setStatus('Redruck gesendet: ' + result.jobId + ' aus Original ' + jobId); await loadQueue(); } catch(e) { setStatus(e.message, true); } }
async function loadBarcodeFormats() { try { const list = await request('/barcodes/formats'); document.getElementById('barcodeFormats').innerHTML = list.map(f => `<span class="badge">${escapeHtml(f.name)} · ${escapeHtml(f.type)}</span> ${escapeHtml(f.description)}`).join('<br>'); } catch(e) { setStatus(e.message, true); } }
function syncTemplateTypeFromContentType() { const ct = val('templateContentType'); document.getElementById('templateType').value = ct.includes('pdf') ? 'PDF' : ct.includes('wordprocessingml') ? 'DOCX' : ct.includes('html') ? 'HTML' : ct.includes('xml') ? 'XML' : 'TEXT'; toggleBinaryNotice(); }
function toggleBinaryNotice() { const bin = val('templateType') === 'DOCX' || val('templateType') === 'PDF' || val('templateType') === 'PDF_TEMPLATE'; document.getElementById('binaryNotice').style.display = bin ? 'block' : 'none'; document.getElementById('templateContent').readOnly = bin; }
function inferContentType(fileName) { const f = String(fileName || '').toLowerCase(); if (f.endsWith('.html') || f.endsWith('.htm')) return 'text/html'; if (f.endsWith('.pdf')) return 'application/pdf'; if (f.endsWith('.docx')) return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; if (f.endsWith('.xml')) return 'application/xml'; if (f.endsWith('.json')) return 'application/json'; return 'text/plain'; }
function inferTemplateType(fileName, contentType) { const f = String(fileName || '').toLowerCase(); if (f.endsWith('.pdf') || String(contentType || '').includes('pdf')) return 'PDF'; if (f.endsWith('.docx') || String(contentType || '').includes('wordprocessingml')) return 'DOCX'; if (String(contentType || '').includes('html')) return 'HTML'; if (f.endsWith('.zpl')) return 'ZPL'; if (f.endsWith('.epl')) return 'EPL'; if (String(contentType || '').includes('xml')) return 'XML'; return 'TEXT'; }
function parsePdfFieldsJson() { const raw = val('pdfFieldsJson').trim(); if (!raw) return {}; try { return JSON.parse(raw); } catch(e) { throw new Error('PDF-Koordinatenfelder enthalten kein gültiges JSON: ' + e.message); } }
function fileToBase64(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(String(reader.result).split(',')[1]); reader.onerror = reject; reader.readAsDataURL(file); }); }
function stripExtension(name) { return String(name || '').replace(/\.[A-Za-z0-9]{1,12}$/, ''); }
function formatBytes(value) { const n = Number(value || 0); if (n < 1024) return n + ' B'; if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB'; return (n / 1024 / 1024).toFixed(1) + ' MB'; }
function escapeHtml(value) { return String(value || '').replace(/[&<>'"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;',"'":'&#39;','"':'&quot;'}[c])); }
function val(id) { return document.getElementById(id).value; }
function valOrEmpty(id) { const el = document.getElementById(id); return el ? el.value : ''; }
document.getElementById('apiToken').value = localStorage.getItem('printservice.token') || '';
loadAuthStatus();
insertHtmlSample(); togglePrinterTypeFields(); loadPrinterDiagnostics(); loadPrinterAliases(); loadTemplates(); loadBarcodeFormats(); loadQueueSettings(); loadQueue();
</script>
</body>
</html>