(function () { "use strict"; var config = window.ObyteQaTool || {}; function onReady(callback) { if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", callback); } else { callback(); } } onReady(function () { document.querySelectorAll("[data-obyte-qa-tool]").forEach(function (root) { if (root.dataset.oqtReady === "1") { return; } root.dataset.oqtReady = "1"; new QaTool(root); }); }); function QaTool(root) { this.root = root; this.template = null; this.dragIndex = -1; this.gitlabTemplatePath = ""; this.gitlabTemplateModule = ""; this.storageKey = "obyteQaToolState:" + window.location.pathname; this.els = this.collectElements(); this.init(); } QaTool.prototype.collectElements = function () { var root = this.root; var field = function (name) { return root.querySelector('[data-field="' + name + '"]'); }; var action = function (name) { return root.querySelector('[data-action="' + name + '"]'); }; return { tplName: field("tplName"), stepSummary: field("stepSummary"), statusTag: field("statusTag"), gitlabTplSelect: field("gitlabTplSelect"), gitlabTplStatus: field("gitlabTplStatus"), docbeeTokenStatus: field("docbeeTokenStatus"), templateFile: field("templateFile"), module: field("module"), moduleVersion: field("moduleVersion"), olmNummer: field("olmNummer"), pbxVersion: field("pbxVersion"), tester: field("tester"), docbeeUrl: field("docbeeUrl"), stepsTable: field("stepsTable"), stepsTableBody: field("stepsTableBody"), loadJson: field("loadJson"), message: field("message"), reloadGitlab: action("reloadGitlab"), pickTemplate: action("pickTemplate"), addStep: action("addStep"), addGroup: action("addGroup"), saveJson: action("saveJson"), pickRun: action("pickRun"), exportAll: action("exportAll"), exportMd: action("exportMd"), exportCsv: action("exportCsv"), printPdf: action("printPdf"), exportTemplateYaml: action("exportTemplateYaml"), pushDocbee: action("pushDocbee") }; }; QaTool.prototype.init = function () { var self = this; if (this.els.tester) { this.els.tester.value = config.tester || ""; } if (this.els.pbxVersion && config.defaultPbxVersion) { this.els.pbxVersion.value = config.defaultPbxVersion; } this.restoreAutosave(); this.bindMetaAutosave(); this.bindControls(); this.updateServiceBadges(); this.updateRunSummary(); if (config.gitlabEnabled) { this.populateGitlabDropdown(); } }; QaTool.prototype.bindMetaAutosave = function () { var self = this; ["input", "change"].forEach(function (eventName) { [self.els.module, self.els.moduleVersion, self.els.olmNummer, self.els.pbxVersion, self.els.docbeeUrl].forEach(function (el) { if (el) { el.addEventListener(eventName, function () { self.updateAutosave(); }); } }); }); }; QaTool.prototype.bindControls = function () { var self = this; bind(this.els.reloadGitlab, "click", function (event) { event.preventDefault(); self.populateGitlabDropdown(); }); bind(this.els.pickTemplate, "click", function () { if (self.els.templateFile) { self.els.templateFile.click(); } }); bind(this.els.templateFile, "change", function (event) { self.loadLocalTemplate(event); }); bind(this.els.gitlabTplSelect, "change", function (event) { self.loadGitlabTemplate(event.target.value); }); bind(this.els.addStep, "click", function (event) { event.preventDefault(); event.stopImmediatePropagation(); self.addStep(); }); bind(this.els.addGroup, "click", function (event) { event.preventDefault(); event.stopImmediatePropagation(); self.addGroup(); }); bind(this.els.saveJson, "click", function () { self.saveJson(); }); bind(this.els.pickRun, "click", function () { if (self.els.loadJson) { self.els.loadJson.click(); } }); bind(this.els.loadJson, "change", function (event) { self.loadRunJson(event); }); bind(this.els.exportAll, "click", function () { self.exportAll(); }); bind(this.els.exportMd, "click", function () { self.exportMd(); }); bind(this.els.exportCsv, "click", function () { self.exportCsv(); }); bind(this.els.printPdf, "click", function () { self.printPdf(); }); bind(this.els.exportTemplateYaml, "click", function () { self.exportTemplateYaml(); }); bind(this.els.pushDocbee, "click", function () { self.postToDocbee(); }); if (this.els.stepsTableBody) { this.els.stepsTableBody.addEventListener("click", function (event) { self.handleRowClick(event); }); this.els.stepsTableBody.addEventListener("input", function () { self.captureEditsIntoTemplate(); self.updateRunSummary(); }); this.els.stepsTableBody.addEventListener("change", function (event) { if (event.target && event.target.matches("select.oqt-status")) { self.updateStatusClass(event.target); self.recomputeGroupStyles(); } self.captureEditsIntoTemplate(); self.updateRunSummary(); }); this.els.stepsTableBody.addEventListener("dragstart", function (event) { self.handleDragStart(event); }); this.els.stepsTableBody.addEventListener("dragend", function (event) { self.handleDragEnd(event); }); this.els.stepsTableBody.addEventListener("dragover", function (event) { self.handleDragOver(event); }); this.els.stepsTableBody.addEventListener("drop", function (event) { self.handleDrop(event); }); } }; QaTool.prototype.api = async function (path, options) { options = options || {}; var response = await fetch((config.restUrl || "") + path, { method: options.method || "GET", credentials: "same-origin", headers: Object.assign({ "Accept": "application/json", "X-WP-Nonce": config.nonce || "" }, options.headers || {}), body: options.body || null }); var text = await response.text(); var data = {}; if (text) { try { data = JSON.parse(text); } catch (error) { data = { message: text }; } } if (!response.ok) { throw new Error(data.message || ("Request failed with HTTP " + response.status)); } return data; }; QaTool.prototype.populateGitlabDropdown = async function () { var select = this.els.gitlabTplSelect; var tag = this.els.gitlabTplStatus; if (!select) { return; } select.innerHTML = ''; if (!config.gitlabEnabled) { select.disabled = true; this.setTag(tag, "GitLab: deaktiviert", "warn"); return; } try { select.disabled = true; this.setTag(tag, "GitLab: lade Liste", ""); var data = await this.api("templates"); var templates = data.templates || []; templates.forEach(function (item) { var option = document.createElement("option"); option.value = item.path; option.textContent = item.name; select.appendChild(option); }); this.setTag(tag, templates.length ? ("GitLab: " + templates.length + " Vorlage(n)") : "GitLab: keine YAMLs", templates.length ? "ok" : "warn"); } catch (error) { this.setTag(tag, "GitLab: Fehler", "bad"); this.showMessage("GitLab-Liste konnte nicht geladen werden: " + error.message, "bad"); } finally { select.disabled = false; } }; QaTool.prototype.loadGitlabTemplate = async function (path) { if (!path) { this.gitlabTemplatePath = ""; this.gitlabTemplateModule = ""; return; } try { this.setTag(this.els.gitlabTplStatus, "GitLab: lade Datei", ""); var data = await this.api("template?path=" + encodeURIComponent(path)); this.loadTemplateText(data.content || "", path.split("/").pop(), data.path || path); this.setTag(this.els.gitlabTplStatus, "GitLab: geladen", "ok"); } catch (error) { this.setTag(this.els.gitlabTplStatus, "GitLab: Fehler", "bad"); this.showMessage("Vorlage aus GitLab konnte nicht geladen werden: " + error.message, "bad"); } }; QaTool.prototype.loadLocalTemplate = async function (event) { var file = event.target.files && event.target.files[0]; if (!file) { return; } try { var text = await file.text(); this.loadTemplateText(text, file.name); this.showMessage("Lokale Vorlage geladen.", "ok"); } catch (error) { this.showMessage("Vorlage konnte nicht geladen werden: " + error.message, "bad"); } finally { event.target.value = ""; } }; QaTool.prototype.loadTemplateText = function (text, fallbackName, sourcePath) { var parsed = parseTemplateText(text); var normalized = normalizeTemplate(parsed, fallbackName || "Template"); this.template = normalized; this.gitlabTemplatePath = sourcePath || ""; this.gitlabTemplateModule = sourcePath ? (normalized.module || "") : ""; if (this.els.tplName) { this.els.tplName.textContent = normalized.name || fallbackName || "Template"; } if (this.els.module) { this.els.module.value = normalized.module || ""; } if (this.els.moduleVersion) { this.els.moduleVersion.value = normalized.module_version || ""; } if (this.els.olmNummer) { this.els.olmNummer.value = normalized.olm_nummer || ""; } if (this.els.pbxVersion) { this.els.pbxVersion.value = normalized.pbx_version || config.defaultPbxVersion || ""; } this.renderSteps(); this.setTag(this.els.statusTag, "Vorlage geladen", "ok"); this.updateAutosave(); }; QaTool.prototype.renderSteps = function () { var body = this.els.stepsTableBody; if (!body || !this.template) { return; } body.innerHTML = ""; var groupCollapsed = false; var self = this; (this.template.steps || []).forEach(function (step) { var kind = step.kind || step.type || "step"; if (kind === "group") { var groupRow = document.createElement("tr"); groupRow.className = "oqt-group-row"; groupRow.dataset.kind = "group"; groupRow.draggable = false; if (step.collapsed) { groupRow.dataset.collapsed = "1"; } groupRow.innerHTML = '' + '
' + '
' + 'Gruppe' + '' + '
' + '
' + '
' + '' + '' + '' + '' + '
' + '
' + '' + '' + '' + '
' + '
' + '
' + ''; body.appendChild(groupRow); groupCollapsed = !!step.collapsed; return; } var row = document.createElement("tr"); row.dataset.kind = "step"; row.draggable = false; if (groupCollapsed) { row.dataset.hidden = "1"; } row.innerHTML = '' + '
' + '
' + 'ID' + '' + '
' + '
' + 'Titel' + '' + '
' + '
' + (step.required ? 'Pflicht' : 'Optional') + '' + '' + '
' + '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + ''; body.appendChild(row); self.updateStatusClass(row.querySelector("select.oqt-status")); }); this.recomputeGroupStyles(); this.updateRunSummary(); }; QaTool.prototype.captureEditsIntoTemplate = function () { if (!this.template || !this.els.stepsTableBody) { return; } var rows = Array.prototype.slice.call(this.els.stepsTableBody.querySelectorAll("tr")).filter(function (row) { return !row.classList.contains("oqt-empty-row"); }); this.template.steps = rows.map(function (row, index) { var kind = row.dataset.kind || "step"; if (kind === "group") { return { kind: "group", title: valueOf(row, ".oqt-group-title"), collapsed: row.dataset.collapsed === "1" }; } return { kind: "step", id: valueOf(row, ".oqt-step-id") || ("step-" + String(index + 1).padStart(3, "0")), title: valueOf(row, ".oqt-title"), expected: valueOf(row, ".oqt-expected"), required: !!(row.querySelector(".oqt-step-required") && row.querySelector(".oqt-step-required").checked), status: valueOf(row, ".oqt-status"), comment: valueOf(row, ".oqt-comment"), evidence: valueOf(row, ".oqt-evidence") }; }); }; QaTool.prototype.ensureTemplate = function () { if (!this.template) { this.template = { name: "Neues Template", module: "", module_version: "", olm_nummer: "", pbx_version: config.defaultPbxVersion || "", steps: [] }; this.gitlabTemplatePath = ""; this.gitlabTemplateModule = ""; if (this.els.tplName) { this.els.tplName.textContent = this.template.name; } } }; QaTool.prototype.addStep = function () { var hadTemplate = !!this.template; this.ensureTemplate(); if (hadTemplate) { this.captureEditsIntoTemplate(); } this.template.steps.push({ kind: "step", id: "", title: "", expected: "", required: true, status: "", comment: "", evidence: "" }); this.renumberSteps(); this.renderSteps(); this.setTag(this.els.statusTag, "Step hinzugefügt", "ok"); }; QaTool.prototype.addGroup = function () { var hadTemplate = !!this.template; this.ensureTemplate(); if (hadTemplate) { this.captureEditsIntoTemplate(); } this.template.steps.push({ kind: "group", title: "Neue Gruppe", collapsed: false }); this.renderSteps(); this.setTag(this.els.statusTag, "Gruppe hinzugefügt", "ok"); }; QaTool.prototype.handleRowClick = function (event) { var button = event.target.closest("[data-row-action]"); if (!button) { return; } event.preventDefault(); this.captureEditsIntoTemplate(); var row = button.closest("tr"); var index = this.rowIndex(row); if (index < 0 || !this.template || !this.template.steps[index]) { return; } var action = button.dataset.rowAction; var item = this.template.steps[index]; if (action === "toggle-group" && item.kind === "group") { item.collapsed = !item.collapsed; this.renderSteps(); return; } if (action === "group-status" && item.kind === "group") { this.applyGroupStatus(index, button.dataset.status || ""); return; } if (action === "delete-group" && item.kind === "group") { var count = this.countStepsUntilNextGroup(index); if (count > 0 && !window.confirm("Diese Gruppe enthält " + count + " Schritt(e). Gruppe wirklich löschen? Die Schritte bleiben erhalten.")) { return; } this.template.steps.splice(index, 1); this.renderSteps(); return; } if (action === "delete-step") { this.template.steps.splice(index, 1); this.renumberSteps(); this.renderSteps(); } }; QaTool.prototype.applyGroupStatus = function (groupIndex, status) { if (["pass", "fail", "skip", "blocked"].indexOf(status) === -1) { return; } var end = this.template.steps.length; for (var i = groupIndex + 1; i < this.template.steps.length; i++) { var kind = this.template.steps[i].kind || this.template.steps[i].type || "step"; if (kind === "group") { end = i; break; } } var willOverwrite = this.template.steps.slice(groupIndex + 1, end).some(function (step) { return (step.kind || "step") === "step" && step.status && step.status !== status; }); if (willOverwrite && !window.confirm("Einige Schritte haben bereits einen anderen Status. Alle auf " + status.toUpperCase() + " setzen?")) { return; } for (var j = groupIndex + 1; j < end; j++) { if ((this.template.steps[j].kind || "step") === "step") { this.template.steps[j].status = status; } } this.renderSteps(); }; QaTool.prototype.countStepsUntilNextGroup = function (groupIndex) { var count = 0; for (var i = groupIndex + 1; i < this.template.steps.length; i++) { var kind = this.template.steps[i].kind || this.template.steps[i].type || "step"; if (kind === "group") { break; } if (kind === "step") { count++; } } return count; }; QaTool.prototype.rowIndex = function (row) { if (!row || !this.els.stepsTableBody) { return -1; } return Array.prototype.indexOf.call(this.els.stepsTableBody.children, row); }; QaTool.prototype.handleDragStart = function (event) { if (!event.target.closest(".oqt-drag-handle")) { event.preventDefault(); return; } var row = event.target.closest("tr"); var index = this.rowIndex(row); if (index < 0) { return; } this.dragIndex = index; row.classList.add("oqt-dragging"); if (event.dataTransfer) { event.dataTransfer.effectAllowed = "move"; event.dataTransfer.setData("text/plain", String(index)); } }; QaTool.prototype.handleDragEnd = function (event) { var row = event.target.closest("tr"); if (row) { row.classList.remove("oqt-dragging"); } this.clearDragMarkers(); this.dragIndex = -1; }; QaTool.prototype.handleDragOver = function (event) { event.preventDefault(); var row = event.target.closest("tr"); if (!row) { return; } this.clearDragMarkers(); row.classList.add("oqt-drag-over"); var rect = row.getBoundingClientRect(); row.dataset.dropPos = (event.clientY - rect.top) < rect.height / 2 ? "before" : "after"; }; QaTool.prototype.handleDrop = function (event) { event.preventDefault(); var row = event.target.closest("tr"); if (!row || !this.template) { return; } this.captureEditsIntoTemplate(); var from = this.dragIndex >= 0 ? this.dragIndex : parseInt(event.dataTransfer && event.dataTransfer.getData("text/plain"), 10); var toBase = this.rowIndex(row); if (!isFinite(from) || from < 0 || toBase < 0) { return; } var to = toBase + (row.dataset.dropPos === "after" ? 1 : 0); if (to === from || to - 1 === from) { this.renderSteps(); return; } var item = this.template.steps.splice(from, 1)[0]; if (to > from) { to--; } this.template.steps.splice(to, 0, item); this.renumberSteps(); this.renderSteps(); }; QaTool.prototype.clearDragMarkers = function () { if (!this.els.stepsTableBody) { return; } this.els.stepsTableBody.querySelectorAll("tr").forEach(function (row) { row.classList.remove("oqt-drag-over"); delete row.dataset.dropPos; }); }; QaTool.prototype.updateStatusClass = function (select) { if (!select) { return; } var row = select.closest("tr"); ["oqt-status-pass", "oqt-status-fail", "oqt-status-skip", "oqt-status-blocked"].forEach(function (className) { select.classList.remove(className); }); ["oqt-row-pass", "oqt-row-fail", "oqt-row-skip", "oqt-row-blocked"].forEach(function (className) { if (row) { row.classList.remove(className); } }); if (!select.value) { return; } select.classList.add("oqt-status-" + select.value); if (row) { row.classList.add("oqt-row-" + select.value); } }; QaTool.prototype.recomputeGroupStyles = function () { if (!this.els.stepsTableBody) { return; } var current = null; var groups = []; this.els.stepsTableBody.querySelectorAll("tr").forEach(function (row) { if ((row.dataset.kind || "step") === "group") { current = { row: row, steps: [] }; groups.push(current); } else if (current) { current.steps.push(row); } }); groups.forEach(function (group) { group.row.classList.remove("is-ok", "is-bad"); var statuses = group.steps.map(function (row) { var select = row.querySelector("select.oqt-status"); return select ? select.value : ""; }); if (!statuses.length) { return; } if (statuses.every(function (status) { return status === "pass"; })) { group.row.classList.add("is-ok"); } else if (statuses.some(function (status) { return status === "fail" || status === "blocked"; })) { group.row.classList.add("is-bad"); } }); }; QaTool.prototype.updateRunSummary = function () { if (!this.els.stepSummary) { return; } var counts = countStatuses(this.template ? this.template.steps : []); if (!counts.total) { this.setTag(this.els.stepSummary, "0 Steps", ""); return; } var text = counts.total + " Step" + (counts.total === 1 ? "" : "s") + " | " + counts.pass + " PASS" + " | " + counts.fail + " FAIL" + " | " + counts.skip + " SKIP" + " | " + counts.blocked + " BLOCK"; if (counts.missingRequired) { text += " | " + counts.missingRequired + " offen"; } this.setTag(this.els.stepSummary, text, counts.missingRequired ? "warn" : (counts.total ? "ok" : "")); }; QaTool.prototype.renumberSteps = function () { if (!this.template || !Array.isArray(this.template.steps)) { return; } var number = 1; this.template.steps.forEach(function (step) { var kind = step.kind || step.type || "step"; if (kind !== "step") { return; } step.id = "step-" + String(number).padStart(3, "0"); number++; }); }; QaTool.prototype.collectRun = function () { if (!this.template) { this.showMessage("Bitte zuerst eine Vorlage laden oder Schritte anlegen.", "bad"); return null; } this.captureEditsIntoTemplate(); this.renumberSteps(); return { name: this.template.name || "", module: value(this.els.module), module_version: value(this.els.moduleVersion), olm_nummer: value(this.els.olmNummer), pbx_version: value(this.els.pbxVersion), tester: value(this.els.tester), docbee_url: value(this.els.docbeeUrl), ts: new Date().toISOString(), steps: clone(this.template.steps || []) }; }; QaTool.prototype.collectTemplateFromDom = function () { this.ensureTemplate(); this.captureEditsIntoTemplate(); this.renumberSteps(); return { name: this.template.name || "QA Template", module: value(this.els.module), module_version: value(this.els.moduleVersion), olm_nummer: value(this.els.olmNummer), pbx_version: value(this.els.pbxVersion), steps: (this.template.steps || []).map(function (step) { var kind = step.kind || step.type || "step"; if (kind === "group") { return { type: "group", title: step.title || "" }; } return { type: "step", id: step.id || "", title: step.title || "", expected: step.expected || "", required: !!step.required }; }) }; }; QaTool.prototype.checkRequired = function (run) { var missing = (run.steps || []).filter(function (step) { return (step.kind || step.type || "step") === "step" && step.required && !step.status; }); if (!missing.length) { return true; } var message = "Folgende Pflichtschritte haben keinen Status:\n" + missing.map(function (step) { return (step.id || "") + " - " + (step.title || ""); }).join("\n"); window.alert(message); this.showMessage("Pflichtschritte ohne Status vorhanden.", "bad"); return false; }; QaTool.prototype.saveJson = function () { var run = this.collectRun(); if (!run || !this.checkRequired(run)) { return; } this.renumberSteps(); download(JSON.stringify(run, null, 2), runBaseName("qa-run", run) + ".json", "application/json"); }; QaTool.prototype.loadRunJson = async function (event) { var file = event.target.files && event.target.files[0]; if (!file) { return; } try { var text = await file.text(); var run = parseTemplateText(text); var isRun = !!(run.tester || run.docbee_url || (run.steps || []).some(function (step) { return step && (step.status || step.comment || step.evidence); })); if (!run || !Array.isArray(run.steps)) { throw new Error("Datei enthält keine steps."); } this.template = normalizeTemplate({ name: run.name || file.name, module: run.module || "", module_version: run.module_version || "", olm_nummer: run.olm_nummer || "", pbx_version: run.pbx_version || "", steps: run.steps }, file.name); this.gitlabTemplatePath = ""; this.gitlabTemplateModule = ""; if (this.els.tplName) { this.els.tplName.textContent = run.name || file.name; } if (this.els.module) { this.els.module.value = run.module || ""; } if (this.els.moduleVersion) { this.els.moduleVersion.value = run.module_version || ""; } if (this.els.olmNummer) { this.els.olmNummer.value = run.olm_nummer || ""; } if (this.els.pbxVersion) { this.els.pbxVersion.value = run.pbx_version || ""; } if (this.els.tester) { this.els.tester.value = isRun ? (run.tester || config.tester || "") : (config.tester || ""); } if (this.els.docbeeUrl) { this.els.docbeeUrl.value = isRun ? (run.docbee_url || "") : ""; } this.renderSteps(); this.setTag(this.els.statusTag, isRun ? "Lauf geladen" : "Template geladen", "ok"); this.updateAutosave(); } catch (error) { this.showMessage("Datei konnte nicht geladen werden: " + error.message, "bad"); } finally { event.target.value = ""; } }; QaTool.prototype.exportMd = function () { var run = this.collectRun(); if (!run || !this.checkRequired(run)) { return; } this.renumberSteps(); var md = "# Testprotokoll\n\n" + "- Modul: " + (run.module || "") + "\n" + "- Modul-Version: " + (run.module_version || "") + "\n" + "- OLM-Nummer: " + (run.olm_nummer || "") + "\n" + "- PBX-Version: " + (run.pbx_version || "") + "\n" + "- DocBee: " + (run.docbee_url || "") + "\n" + "- Tester: " + (run.tester || "") + "\n" + "- Datum: " + new Date(run.ts).toLocaleString("de-DE") + "\n\n" + "| Schritt | Erwartung | Status | Kommentar | Evidenz |\n" + "|---|---|---|---|---|\n"; run.steps.forEach(function (step) { if ((step.kind || step.type || "step") === "group") { md += "| **-- " + mdCell(step.title) + " --** | | | | |\n"; return; } md += "| **" + mdCell(step.id) + " - " + mdCell(step.title) + (step.required ? " [required]" : "") + "** | " + mdCell(step.expected) + " | " + mdCell(step.status) + " | " + mdCell(step.comment) + " | " + mdCell(step.evidence) + " |\n"; }); download(md, runBaseName("qa-report", run) + ".md", "text/markdown"); }; QaTool.prototype.exportCsv = function () { var run = this.collectRun(); if (!run || !this.checkRequired(run)) { return; } this.renumberSteps(); var meta = [ ["module", run.module || ""], ["module_version", run.module_version || ""], ["olm_nummer", run.olm_nummer || ""], ["pbx_version", run.pbx_version || ""], ["docbee_url", run.docbee_url || ""], ["tester", run.tester || ""], ["date", new Date(run.ts).toLocaleString("de-DE")] ].map(function (row) { return row[0] + ";" + csvCell(row[1]); }).join("\n"); var rows = [["id", "title", "expected", "required", "status", "comment", "evidence"]]; run.steps.forEach(function (step) { if ((step.kind || step.type || "step") === "group") { rows.push(["", "### Gruppe: " + (step.title || ""), "", "", "", "", ""]); return; } rows.push([step.id, step.title, step.expected, step.required ? "true" : "false", step.status, step.comment, step.evidence]); }); var table = rows.map(function (row) { return row.map(csvCell).join(";"); }).join("\n"); download(meta + "\n\n" + table, runBaseName("qa-report", run) + ".csv", "text/csv"); }; QaTool.prototype.ensureJsPdf = async function () { if (window.jspdf && window.jspdf.jsPDF) { return true; } var urls = config.jsPdfUrls || []; for (var i = 0; i < urls.length; i++) { try { await loadScript(urls[i]); } catch (error) { // Try next CDN. } if (window.jspdf && window.jspdf.jsPDF) { break; } } if (!(window.jspdf && window.jspdf.jsPDF)) { return false; } try { var test = new window.jspdf.jsPDF(); if (typeof test.autoTable !== "function") { var tableUrls = config.autoTableUrls || []; for (var j = 0; j < tableUrls.length; j++) { try { await loadScript(tableUrls[j]); } catch (error2) { // PDF export still works without AutoTable. } test = new window.jspdf.jsPDF(); if (typeof test.autoTable === "function") { break; } } } } catch (error3) { // Basic jsPDF is enough for a fallback PDF. } return true; }; QaTool.prototype.generatePdfBlob = async function (run) { var ready = await this.ensureJsPdf(); if (!ready) { throw new Error("PDF-Bibliothek konnte nicht geladen werden."); } var jsPDF = window.jspdf.jsPDF; var doc = new jsPDF({ orientation: "landscape", unit: "mm", format: "a4", compress: true }); var pageW = doc.internal.pageSize.getWidth(); var pageH = doc.internal.pageSize.getHeight(); var marginL = 14; var marginR = 14; var xRight = pageW - marginR; var footerY = pageH - 13; var muted = [102, 102, 102]; var headFill = [232, 247, 253]; var ok = [24, 143, 92]; var fail = [196, 59, 59]; var skip = [103, 113, 122]; var blocked = [245, 156, 0]; var logoDataUrl = null; try { logoDataUrl = await imageToDataUrl(config.printLogoUrl || config.logoUrl || ""); } catch (error) { logoDataUrl = null; } doc.setFont("helvetica", "bold"); doc.setFontSize(18); doc.setTextColor(52, 53, 55); doc.text("QA Report", marginL, 16); if (logoDataUrl) { try { var props = doc.getImageProperties ? doc.getImageProperties(logoDataUrl) : null; var ratio = props && props.width ? props.height / props.width : 0.38; var logoW = 32; var logoH = logoW * ratio; if (logoH > 13) { logoH = 13; logoW = logoH / ratio; } doc.addImage(logoDataUrl, "PNG", xRight - logoW, 6.5, logoW, logoH, undefined, "FAST"); } catch (error2) { // Logo is decorative; keep the PDF export working without it. } } doc.setDrawColor(0, 167, 230); doc.setLineWidth(0.45); doc.line(marginL, 18, xRight, 18); var meta = [ ["Modul", sanitizePdfText(run.module)], ["Modul-Version", sanitizePdfText(run.module_version)], ["OLM-Nummer", sanitizePdfText(run.olm_nummer || "-")], ["PBX-Version", sanitizePdfText(run.pbx_version)], ["Tester", sanitizePdfText(run.tester)], ["DocBee", sanitizePdfText(run.docbee_url || "-")] ]; var y = 25; doc.setFontSize(10); meta.forEach(function (row) { doc.setFont("helvetica", "bold"); doc.setTextColor(muted[0], muted[1], muted[2]); doc.text(row[0] + ":", marginL, y); doc.setFont("helvetica", "normal"); doc.setTextColor(0, 0, 0); doc.text(row[1] || "-", 50, y); y += 6; }); var rows = []; var currentGroup = "-"; var index = 0; (run.steps || []).forEach(function (step) { if ((step.kind || step.type || "step") === "group") { currentGroup = sanitizePdfText(step.title || "-"); return; } index++; rows.push({ nr: index, group: currentGroup, id: sanitizePdfText(step.id), title: sanitizePdfText(step.title) + (step.required ? " [required]" : ""), expected: sanitizePdfText(step.expected), status: sanitizePdfText((step.status || "").toLowerCase()), comment: sanitizePdfText(step.comment) }); }); if (typeof doc.autoTable === "function") { doc.autoTable({ startY: Math.max(y + 4, 25), tableWidth: pageW - (marginL + marginR), head: [["#", "Gruppe", "Step-ID", "Titel", "Erwartet", "Status", "Kommentar"]], body: rows.map(function (row) { return [row.nr, row.group, row.id, row.title, row.expected, row.status, row.comment]; }), styles: { font: "helvetica", fontSize: 9, cellPadding: 2, overflow: "linebreak", valign: "top" }, headStyles: { fillColor: headFill, textColor: [52, 53, 55], halign: "left" }, columnStyles: { 0: { halign: "right", cellWidth: 8 }, 1: { cellWidth: 36 }, 2: { cellWidth: 24 }, 3: { cellWidth: 62 }, 4: { cellWidth: 76, font: "courier", fillColor: [250, 250, 250] }, 5: { cellWidth: 20 }, 6: { cellWidth: 36 } }, didParseCell: function (data) { if (data.section === "body" && data.column.index === 5) { var value = String(data.cell.raw || "").toLowerCase(); if (value === "pass") { data.cell.styles.textColor = ok; } else if (value === "fail") { data.cell.styles.textColor = fail; } else if (value === "skip" || value === "na") { data.cell.styles.textColor = skip; } else if (value === "blocked") { data.cell.styles.textColor = blocked; } } }, margin: { left: marginL, right: marginR }, pageBreak: "auto" }); } else { doc.setFont("helvetica", "normal"); doc.setFontSize(9); y += 6; rows.forEach(function (row) { if (y > pageH - 20) { doc.addPage(); y = 16; } doc.text([row.nr + " " + row.id + " " + row.status, row.group + " | " + row.title, row.expected, row.comment].join(" - "), marginL, y, { maxWidth: pageW - marginL - marginR }); y += 8; }); } var pageCount = doc.getNumberOfPages(); var timestamp = new Date().toLocaleString("de-DE"); for (var p = 1; p <= pageCount; p++) { doc.setPage(p); doc.setFont("helvetica", "normal"); doc.setFontSize(8); doc.setTextColor(muted[0], muted[1], muted[2]); doc.text("Seite " + p + " / " + pageCount, xRight, footerY, { align: "right" }); doc.text(timestamp, marginL, footerY); } return doc.output("blob"); }; QaTool.prototype.printPdf = async function () { var run = this.collectRun(); if (!run || !this.checkRequired(run)) { return; } this.renumberSteps(); try { var pdfBlob = await this.generatePdfBlob(run); download(pdfBlob, runBaseName("qa-report", run) + ".pdf", "application/pdf"); this.showMessage("PDF wurde erzeugt.", "ok"); return; } catch (error) { this.showMessage("PDF konnte nicht erzeugt werden: " + error.message, "bad"); return; } }; QaTool.prototype.exportAll = async function () { var run = this.collectRun(); if (!run || !this.checkRequired(run)) { return; } var button = this.els.exportAll; try { if (button) { button.disabled = true; button.classList.add("is-busy"); } this.showMessage("Export läuft: PDF wird erzeugt und WordPress speichert den Lauf...", ""); var pdfBlob = null; try { pdfBlob = await this.generatePdfBlob(run); } catch (pdfError) { this.showMessage("PDF konnte nicht erzeugt werden, Report wird trotzdem gespeichert: " + pdfError.message, "bad"); } var form = new FormData(); form.append("run", JSON.stringify(run)); if (pdfBlob) { form.append("pdf", pdfBlob, runBaseName("qa-report", run) + ".pdf"); } var data = await this.api("export", { method: "POST", body: form }); var parts = []; if (data.report_id) { parts.push("Report-ID: " + data.report_id); } if (data.pdf_stored) { parts.push("PDF geschuetzt gespeichert"); } if (data.docbee && data.docbee.ok) { var docbeeLink = data.docbee.url ? ' DocBee öffnen' : ""; parts.push("DocBee gepostet." + docbeeLink); } else if (data.docbee && data.docbee.message) { parts.push("DocBee Fehler: " + escHtml(data.docbee.message)); } this.showMessage("Export fertig. " + parts.join(" | "), "ok", true); } catch (error) { this.showMessage("Export fehlgeschlagen: " + error.message, "bad"); } finally { if (button) { button.disabled = false; button.classList.remove("is-busy"); } } }; QaTool.prototype.exportTemplateYaml = async function () { var template = this.collectTemplateFromDom(); if (!template.module || !template.module_version || !template.pbx_version) { this.showMessage("Bitte Modul, Modul-Version und PBX-Version angeben.", "bad"); return; } var yaml = dumpTemplateYaml(template); var button = this.els.exportTemplateYaml; if (!config.gitlabWritesEnabled) { download(yaml, runBaseName("qa-template", template) + ".yaml", "text/yaml"); this.showMessage("GitLab-Schreiben ist deaktiviert. Template wurde lokal gespeichert.", "ok"); this.renderSteps(); return; } try { if (button) { button.disabled = true; button.classList.add("is-busy"); } this.showMessage("GitLab: Template wird geschrieben...", ""); var sourcePath = this.gitlabTemplatePath && sameModuleName(template.module, this.gitlabTemplateModule) ? this.gitlabTemplatePath : ""; var data = await this.api("gitlab/template", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ template: template, yaml: yaml, source_path: sourcePath }) }); this.gitlabTemplatePath = data.path || sourcePath || ""; this.gitlabTemplateModule = template.module || ""; this.showMessage("GitLab: Template gespeichert: " + (data.path || ""), "ok"); } catch (error) { this.showMessage("GitLab-Push fehlgeschlagen: " + error.message, "bad"); } finally { if (button) { button.disabled = false; button.classList.remove("is-busy"); } this.renderSteps(); } }; QaTool.prototype.postToDocbee = async function () { var run = this.collectRun(); if (!run || !this.checkRequired(run)) { return; } if (!config.docbeeEnabled || !config.docbeeConfigured) { this.showMessage("DocBee ist im WordPress Backend nicht vollständig konfiguriert.", "bad"); return; } var button = this.els.pushDocbee; try { if (button) { button.disabled = true; button.classList.add("is-busy"); } this.showMessage("DocBee: sende Report...", ""); var data = await this.api("docbee/message", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ run: run }) }); var suffix = data.url ? ' Oeffnen' : ""; this.showMessage("DocBee: Report wurde gepostet." + suffix, "ok", true); } catch (error) { this.showMessage("DocBee-Request fehlgeschlagen: " + error.message, "bad"); } finally { if (button) { button.disabled = false; button.classList.remove("is-busy"); } } }; QaTool.prototype.updateAutosave = function () { var state = { module: value(this.els.module), moduleVersion: value(this.els.moduleVersion), olmNummer: value(this.els.olmNummer), pbxVersion: value(this.els.pbxVersion), docbeeUrl: value(this.els.docbeeUrl) }; try { window.localStorage.setItem(this.storageKey, JSON.stringify(state)); } catch (error) { // Ignore private browsing quota errors. } }; QaTool.prototype.restoreAutosave = function () { try { var state = JSON.parse(window.localStorage.getItem(this.storageKey) || "{}"); if (state.module && this.els.module) { this.els.module.value = state.module; } if (state.moduleVersion && this.els.moduleVersion) { this.els.moduleVersion.value = state.moduleVersion; } if (state.olmNummer && this.els.olmNummer) { this.els.olmNummer.value = state.olmNummer; } if (state.pbxVersion && this.els.pbxVersion) { this.els.pbxVersion.value = state.pbxVersion; } if (state.docbeeUrl && this.els.docbeeUrl) { this.els.docbeeUrl.value = state.docbeeUrl; } } catch (error) { // Ignore broken old autosave data. } }; QaTool.prototype.updateServiceBadges = function () { if (config.gitlabEnabled) { this.setTag(this.els.gitlabTplStatus, "GitLab: bereit", ""); } else { this.setTag(this.els.gitlabTplStatus, "GitLab: deaktiviert", "warn"); } if (!config.docbeeEnabled) { this.setTag(this.els.docbeeTokenStatus, "DocBee: deaktiviert", "warn"); } else if (config.docbeeConfigured) { this.setTag(this.els.docbeeTokenStatus, "DocBee: konfiguriert", "ok"); } else { this.setTag(this.els.docbeeTokenStatus, "DocBee: fehlt", "warn"); } }; QaTool.prototype.setTag = function (tag, text, state) { if (!tag) { return; } tag.textContent = text; tag.classList.remove("is-ok", "is-warn", "is-bad"); if (state) { tag.classList.add("is-" + state); } }; QaTool.prototype.showMessage = function (message, state, allowHtml) { if (!this.els.message) { return; } this.els.message.classList.remove("is-ok", "is-bad"); if (state) { this.els.message.classList.add("is-" + state); } if (allowHtml) { this.els.message.innerHTML = message; } else { this.els.message.textContent = message; } }; function bind(el, eventName, callback) { if (el) { el.addEventListener(eventName, callback); } } function value(el) { return el ? String(el.value || "").trim() : ""; } function valueOf(root, selector) { return value(root.querySelector(selector)); } function clone(valueToClone) { return JSON.parse(JSON.stringify(valueToClone)); } function parseTemplateText(text) { var trimmed = String(text || "").trim(); if (!trimmed) { throw new Error("Datei ist leer."); } if (window.jsyaml && typeof window.jsyaml.load === "function") { try { var yaml = window.jsyaml.load(trimmed); if (yaml && typeof yaml === "object") { return yaml; } } catch (error) { // Try JSON and the small fallback parser below. } } if (trimmed.charAt(0) === "{" || trimmed.charAt(0) === "[") { return JSON.parse(trimmed); } try { return JSON.parse(trimmed); } catch (error) { return parseSimpleYaml(trimmed); } } function normalizeTemplate(input, fallbackName) { if (!input || !Array.isArray(input.steps)) { throw new Error('Ungültige Vorlage: "steps" fehlt.'); } return { name: stringOr(input.name, fallbackName || "Template"), module: stringOr(input.module, ""), module_version: stringOr(input.module_version, ""), olm_nummer: stringOr(input.olm_nummer || input.olm || input.olmNumber, ""), pbx_version: stringOr(input.pbx_version, ""), steps: input.steps.map(function (step, index) { var kind = step.kind || step.type || "step"; if (kind === "group") { return { kind: "group", title: stringOr(step.title || step.name, "Gruppe " + (index + 1)), collapsed: !!step.collapsed }; } return { kind: "step", id: stringOr(step.id, "step-" + String(index + 1).padStart(3, "0")), title: stringOr(step.title, ""), expected: stringOr(step.expected, ""), required: toBool(step.required), status: stringOr(step.status, ""), comment: stringOr(step.comment, ""), evidence: stringOr(step.evidence, "") }; }) }; } function parseSimpleYaml(text) { var lines = String(text || "").replace(/\r\n?/g, "\n").split("\n"); var out = {}; var steps = null; var current = null; var index = 0; while (index < lines.length) { var raw = lines[index]; var trimmed = raw.trim(); var indent = countIndent(raw); if (!trimmed || trimmed.charAt(0) === "#") { index++; continue; } if (indent === 0) { var top = splitKeyValue(trimmed); if (!top) { index++; continue; } if (top.key === "steps") { steps = []; out.steps = steps; current = null; index++; continue; } if (top.value === "|" || top.value === ">") { var topBlock = readBlock(lines, index + 1, indent, top.value === ">"); out[top.key] = topBlock.value; index = topBlock.next; continue; } out[top.key] = parseScalar(top.value); index++; continue; } if (steps && indent >= 2) { if (trimmed.indexOf("- ") === 0) { current = {}; steps.push(current); var rest = trimmed.slice(2).trim(); if (rest) { var inline = splitKeyValue(rest); if (inline) { current[inline.key] = parseScalar(inline.value); } } index++; continue; } if (current) { var pair = splitKeyValue(trimmed); if (pair) { if (pair.value === "|" || pair.value === ">") { var block = readBlock(lines, index + 1, indent, pair.value === ">"); current[pair.key] = block.value; index = block.next; continue; } current[pair.key] = parseScalar(pair.value); } } } index++; } return out; } function countIndent(line) { var match = String(line || "").match(/^ */); return match ? match[0].length : 0; } function splitKeyValue(text) { var position = text.indexOf(":"); if (position < 0) { return null; } return { key: text.slice(0, position).trim(), value: text.slice(position + 1).trim() }; } function readBlock(lines, start, parentIndent, folded) { var collected = []; var index = start; var minIndent = null; while (index < lines.length) { var raw = lines[index]; var trimmed = raw.trim(); var indent = countIndent(raw); if (trimmed && indent <= parentIndent) { break; } if (!trimmed) { collected.push(""); index++; continue; } if (minIndent === null || indent < minIndent) { minIndent = indent; } collected.push(raw); index++; } minIndent = minIndent || 0; var value = collected.map(function (line) { return line ? line.slice(minIndent) : ""; }).join("\n").trim(); if (folded) { value = value.replace(/\n+/g, " "); } return { value: value, next: index }; } function parseScalar(value) { value = String(value || "").trim(); if (value === "") { return ""; } if (value === "true") { return true; } if (value === "false") { return false; } if (value === "null" || value === "~") { return ""; } if ((value.charAt(0) === '"' && value.charAt(value.length - 1) === '"') || (value.charAt(0) === "'" && value.charAt(value.length - 1) === "'")) { value = value.slice(1, -1); } return value.replace(/\\"/g, '"').replace(/\\n/g, "\n"); } function dumpTemplateYaml(template) { if (window.jsyaml && typeof window.jsyaml.dump === "function") { return window.jsyaml.dump(template, { lineWidth: 100 }); } var lines = [ 'name: "' + yamlString(template.name || "") + '"', 'module: "' + yamlString(template.module || "") + '"', 'module_version: "' + yamlString(template.module_version || "") + '"', 'olm_nummer: "' + yamlString(template.olm_nummer || "") + '"', 'pbx_version: "' + yamlString(template.pbx_version || "") + '"', "steps:" ]; (template.steps || []).forEach(function (step) { if (step.type === "group") { lines.push(' - type: "group"'); lines.push(' title: "' + yamlString(step.title || "") + '"'); return; } lines.push(' - type: "step"'); lines.push(' id: "' + yamlString(step.id || "") + '"'); lines.push(' title: "' + yamlString(step.title || "") + '"'); lines.push(' expected: "' + yamlString(step.expected || "") + '"'); lines.push(" required: " + (step.required ? "true" : "false")); }); return lines.join("\n") + "\n"; } function yamlString(valueToEscape) { return String(valueToEscape || "").replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n"); } function toBool(valueToConvert) { if (typeof valueToConvert === "boolean") { return valueToConvert; } return ["1", "true", "yes", "ja"].indexOf(String(valueToConvert || "").toLowerCase()) !== -1; } function stringOr(valueToConvert, fallback) { if (valueToConvert === undefined || valueToConvert === null) { return fallback; } return String(valueToConvert); } function loadScript(url) { return new Promise(function (resolve, reject) { var script = document.createElement("script"); script.src = url; script.async = true; script.onload = resolve; script.onerror = reject; document.head.appendChild(script); }); } function imageToDataUrl(src) { return new Promise(function (resolve, reject) { if (!src) { reject(new Error("No image source.")); return; } var image = new Image(); image.crossOrigin = "anonymous"; image.onload = function () { try { var canvas = document.createElement("canvas"); canvas.width = image.naturalWidth || image.width; canvas.height = image.naturalHeight || image.height; var context = canvas.getContext("2d"); context.drawImage(image, 0, 0); resolve(canvas.toDataURL("image/png")); } catch (error) { reject(error); } }; image.onerror = reject; image.src = src; }); } function replaceSymbols(valueToClean) { return String(valueToClean === undefined || valueToClean === null ? "" : valueToClean) .replace(/[\u2192\u21A6\u21E8\u27A1\u2794\u27F6\u27F7\u27F9\u279D\u279E\u279F\u27A0]/g, "->") .replace(/[\u2190\u21A4\u21E6\u2B05\u27F5]/g, "<-") .replace(/[\u2194\u21D4\u27F7\u27F8\u2B04]/g, "<->") .replace(/[\u2013\u2014\u2011]/g, "-") .replace(/\u2026/g, "...") .replace(/\u00D7/g, "x"); } function toPdfSafe(valueToClean) { return String(valueToClean || "").replace(/[^\x09\x0A\x0D\x20-\x7E\u00A0-\u00FF]/g, "?"); } function sanitizePdfText(valueToClean) { return toPdfSafe(replaceSymbols(String(valueToClean === undefined || valueToClean === null ? "" : valueToClean) .replace(/\u00A0/g, " ") .replace(/[\u201C\u201D]/g, '"') .replace(/[\u2018\u2019]/g, "'") .replace(/\s+/g, " ") .trim())); } function download(content, filename, type) { var blob = content instanceof Blob ? content : new Blob([content], { type: type }); var url = URL.createObjectURL(blob); var link = document.createElement("a"); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); link.remove(); setTimeout(function () { URL.revokeObjectURL(url); }, 1500); } function runBaseName(prefix, run) { return [ prefix, safeName(run.module || "modul"), safeName(run.module_version || "version"), safeName(run.pbx_version || "pbx") ].join("-"); } function safeName(valueToClean) { return String(valueToClean || "") .trim() .toLowerCase() .replace(/\s+/g, "-") .replace(/[^\w.-]/g, "") || "qa"; } function sameModuleName(left, right) { return String(left || "").trim() === String(right || "").trim(); } function mdCell(valueToEscape) { return String(valueToEscape || "").replace(/\|/g, "\\|").replace(/\n/g, " "); } function csvCell(valueToEscape) { return '"' + String(valueToEscape === undefined || valueToEscape === null ? "" : valueToEscape).replace(/"/g, '""') + '"'; } function countStatuses(steps) { var counts = { total: 0, pass: 0, fail: 0, skip: 0, blocked: 0, missingRequired: 0 }; (steps || []).forEach(function (step) { if ((step.kind || step.type || "step") !== "step") { return; } counts.total++; if (Object.prototype.hasOwnProperty.call(counts, step.status)) { counts[step.status]++; } if (step.required && !step.status) { counts.missingRequired++; } }); return counts; } function escHtml(valueToEscape) { return String(valueToEscape === undefined || valueToEscape === null ? "" : valueToEscape) .replace(/&/g, "&") .replace(//g, ">"); } function escAttr(valueToEscape) { return escHtml(valueToEscape).replace(/"/g, """); } })();