// auto-split module async function tryLoad(urls) { for (const u of urls) { try { await loadScript(u); } catch (_) {} if (window.jspdf && window.jspdf.jsPDF) return true; } return !!(window.jspdf && window.jspdf.jsPDF); } function go(){ try{ window.focus(); }catch(e){} setTimeout(function(){ window.print(); }, 120); } function applyRun(run) { template = { name: run.name || '', steps: (run.steps || []).map(s => { const k = s.kind || s.type || 'step'; if (k === 'group') return { kind: 'group', title: s.title || '' }; return { kind: 'step', id: s.id, title: s.title, expected: s.expected, required: !!s.required, status: s.status || '', comment: s.comment || '', evidence: s.evidence || '' }; }) }; if (els.tplName) els.tplName.textContent = run.name || '—'; if (els.module) els.module.value = run.module || ''; if (els.moduleVersion) els.moduleVersion.value = run.module_version || ''; if (els.pbxVersion) els.pbxVersion.value = run.pbx_version || ''; if (els.tester) els.tester.value = run.tester || ''; if (els.docbeeUrl) els.docbeeUrl.value = run.docbee_url || ''; renderSteps(template.steps); recomputeGroupStyles(); if (els.statusTag) els.statusTag.textContent = 'Lauf geladen'; updateAutosave(); } function applyTemplateOnly(tplObj) { template = { name: tplObj.name || '', steps: (tplObj.steps || []).map(s => { const k = s.kind || s.type || 'step'; if (k === 'group') return { kind: 'group', title: s.title || '' }; return { kind: 'step', id: s.id || '', title: s.title || '', expected: s.expected || '', required: !!s.required, status: '', comment: '', evidence: '' }; }) }; if (els.tplName) els.tplName.textContent = template.name || '—'; if (els.module) els.module.value = tplObj.module || ''; if (els.moduleVersion) els.moduleVersion.value = tplObj.module_version || ''; if (els.pbxVersion) els.pbxVersion.value = tplObj.pbx_version || ''; renderSteps(template.steps); recomputeGroupStyles(); if (els.statusTag) els.statusTag.textContent = 'Template geladen'; updateAutosave(); } function collectTemplateFromDOM() { if (!template) return { name: '', module: '', module_version: '', pbx_version: '', olm_nummer: '', steps: [] }; captureEditsIntoTemplate(); renumberSteps(); return { name: template?.name || els.tplName?.textContent || '', module: els.module.value, module_version: els.moduleVersion.value, pbx_version: els.pbxVersion.value, olm_nummer: els.olmNummer ? els.olmNummer.value : '', steps: template.steps.map(s => { if ((s.kind || 'step') === 'group') return { type: 'group', title: s.title }; return { type: 'step', id: s.id, title: s.title, expected: s.expected, required: !!s.required }; }) }; } function collectRun() { if (!template) return null; captureEditsIntoTemplate(); return { name: template?.name || '', module: els.module.value, module_version: els.moduleVersion.value, pbx_version: els.pbxVersion.value, olm_nummer: els.olmNummer ? els.olmNummer.value : '', tester: els.tester.value, docbee_url: els.docbeeUrl.value, ts: new Date().toISOString(), steps: [...template.steps] }; } function appendResultLink(createdJSON) { let link = null; try { const j = JSON.parse(createdJSON || "{}"); link = j?.link || null; } catch {} // Ticket-ID aus dem Eingabefeld (falls vorhanden) extrahieren const ticketId = extractTicketId(els.docbeeUrl?.value || ''); // Ziel-URL bestimmen: // 1) Falls API bereits einen Pfad liefert (z. B. "ticket/show/123"), normieren. // 2) Sonst UI-Link über bekannte Struktur /ticket/show/%ID% bauen. let url = null; if (link) { if (/^https?:\/\//i.test(link)) { url = link; // bereits absolute URL } else { url = DOCBEE_UI_BASE.replace(/\/+$/, '') + '/' + String(link).replace(/^\/+/, ''); } } else if (ticketId) { url = `${DOCBEE_UI_BASE.replace(/\/+$/,'')}/ticket/show/${ticketId}`; } if (!url) return; const hint = document.createElement('div'); hint.className = 'docbee-hint'; hint.innerHTML = `✅ Angelegt: ${url}`; document.querySelector('.actions')?.appendChild(hint); } function formatDocBeeMessage(run) { const pad = (s, n) => (s || '').length > n ? (s || '').slice(0, n - 1) + '…' : (s || '').padEnd(n, ' '); const fmtDate = new Date(run.ts).toLocaleString('de-DE'); // Kurz-Summary (nur echte Steps zählen) const counts = { pass: 0, fail: 0, skip: 0, blocked: 0 }; (run.steps || []).forEach(s => { const k = s.kind || s.type || 'step'; if (k !== 'step') return; if (counts[s.status] !== undefined) counts[s.status]++; }); const summary = `✅ ${counts.pass} | ❌ ${counts.fail} | ⏭️ ${counts.skip} | ⛔ ${counts.blocked}`; // Kopf + Metadaten (out VOR jeglicher Nutzung initialisieren) let out = ''; out += `QA REPORT\n`; out += `===========\n`; out += `Modul: ${run.module || ''}\n`; out += `Modul-Version:${run.module_version || ''}\n`; out += `PBX-Version: ${run.pbx_version || ''}\n`; out += `Tester: ${run.tester || ''}\n`; if (run.docbee_url) out += `Ticket: ${run.docbee_url}\n`; out += `Datum: ${fmtDate}\n\n`; out += `Übersicht: ${summary}\n\n`; // Tabelle (monospace-geeignet) const SEP = '────────────────────────────────────────────────────────────────────────'; out += `${SEP}\n`; out += `${pad('Schritt', 12)} ${pad('Status', 7)} ${pad('Titel', 52)}\n`; out += `${SEP}\n`; (run.steps || []).forEach(s => { const k = s.kind || s.type || 'step'; if (k === 'group') { out += `\n## ${s.title || ''}\n\n`; return; } const st = (s.status || '').toUpperCase(); // PASS/FAIL/SKIP/BLOCKED/… const stShort = st === 'BLOCKED' ? 'BLOCK' : st; const SMAP = { pass: '✅', fail: '❌', skip: '⏭️', blocked: '⛔' }; const stLabel = (SMAP[(s.status || '').toLowerCase()] ? SMAP[(s.status || '').toLowerCase()] + ' ' : '') + stShort; const req = s.required ? '📌 ' : ''; out += `${pad(s.id || '', 12)} ${pad(stLabel, 9)} ${pad(req + (s.title || ''), 50)}\n`; if (s.comment) out += ` • Kommentar: ${s.comment}\n`; if (s.evidence) out += ` • Evidenz: ${s.evidence}\n`; }); out += `${SEP}\n`; out += `Legende: PASS=✅, FAIL=❌, SKIP=⏭️, BLOCK=⛔\n`; return out; } async function postToDocBee() { const run = collectRun(); if (!run) return; if (!checkRequired(run)) return; const ticketId = extractTicketId(els.docbeeUrl?.value || ''); if (!ticketId) { alert("Keine Ticket-ID in der DocBee-URL gefunden."); return; } if (!hasToken()) { alert("Kein API-Token konfiguriert."); return; } // Kommentarinhalt (Markdown) const content = formatDocBeeMessage(run); // alten Vorgangs-Status holen (Vorgangs-Status = ticketStatus) const tGet = await getJSON(`${DOCBEE_BASEURL}/restApi/v1/ticket/${ticketId}?fields=ticketStatus`); const prevStatusId = tGet?.json?.ticketStatus ?? null; // Busy-State els.btnPushDocBee?.setAttribute('disabled', ''); els.btnPushDocBee?.classList.add('is-busy'); try { // *** Message posten (muss Message sein) *** const url = `${DOCBEE_BASEURL}/restApi/v1/ticket/${ticketId}/message`; const rMsg = await postJSON(url, { content: content, subject: `QA Report ${ticketId}`, internal: true, hidden: false }); if (rMsg.ok) { // kurze Wartezeit, dann Status ggf. zurücksetzen (gegen Auto-"Antwort erhalten") await sleep(800); if (prevStatusId != null) { await restoreTicketStatus(ticketId, prevStatusId); } appendResultLink(rMsg.text); alert("Nachricht im Ticket angelegt (Status beibehalten)."); return; } if (ENABLE_FALLBACK_NOTE) { const rNote = await postJSON(`${DOCBEE_BASEURL}/restApi/v1/note`, { note: { ticket: { id: Number(ticketId) }, subject: `QA Report ${ticketId}`, text: content, internal: false } }); if (rNote.ok) { appendResultLink(rNote.text); alert("Notiz angelegt (Fallback)."); return; } alert(`Fehler: MSG ${rMsg.status}, NOTE ${rNote.status}`); } else { alert(`Nachricht fehlgeschlagen (Status ${rMsg.status}). Details siehe Konsole.`); } } catch (e) { alert("DocBee-Request fehlgeschlagen: " + String(e)); } finally { els.btnPushDocBee?.removeAttribute('disabled'); els.btnPushDocBee?.classList.remove('is-busy'); } }