// ===================== QA App – komplette app.js ===================== // === DocBee: Token HIER eintragen === const DOCBEE_TOKEN = window.DOCBEE_TOKEN || ""; // DocBee Token eintragen" const ENABLE_FALLBACK_NOTE = false; // auf true setzen, falls bei Fehler automatisch Notiz angelegt werden soll window.addEventListener('error', e => { try { console.error(e.error || e.message || e); } catch (_) {} alert('JS-Fehler: ' + (e.message || e)); }); let template = null; // {name,module,module_version,pbx_version,steps:[...]} // === DOM-Refs === const els = { yamlFile: document.getElementById('yamlFile'), stepsTableBody: document.querySelector('#stepsTable tbody'), tplName: document.getElementById('tplName'), statusTag: document.getElementById('statusTag'), module: document.getElementById('module'), moduleVersion: document.getElementById('moduleVersion'), pbxVersion: document.getElementById('pbxVersion'), tester: document.getElementById('tester'), olmNummer: document.getElementById('olmNummer'), docbeeUrl: document.getElementById('docbeeUrl'), btnAddStep: document.getElementById('btnAddStep'), btnSaveJSON: document.getElementById('btnSaveJSON'), loadJSON: document.getElementById('loadJSON'), btnAddGroup: document.getElementById('btnAddGroup'), btnExportTemplateYAML: document.getElementById('btnExportTemplateYAML'), btnExportAll: document.getElementById('btnExportAll'), gitlabTplSelect: document.getElementById('gitlabTplSelect'), gitlabTplStatus: document.getElementById('gitlabTplStatus'), docbeeTokenStatus: document.getElementById('docbeeTokenStatus'), btnGitlabReload: document.getElementById('btnGitlabReload'), }; // === Drag Guard + Observer: Nur Drag über expliziten Griff zulassen === function enforceDragHandleOnly() { if (!els.stepsTableBody) return; els.stepsTableBody.querySelectorAll('tr[draggable="true"]').forEach(tr => tr.setAttribute('draggable', 'false')); els.stepsTableBody.querySelectorAll('.drag-handle').forEach(h => h.setAttribute('draggable', 'true')); } document.addEventListener('DOMContentLoaded', () => { if (els.stepsTableBody) { const mo = new MutationObserver(() => enforceDragHandleOnly()); mo.observe(els.stepsTableBody, { childList: true, subtree: true }); enforceDragHandleOnly(); } }); document.addEventListener('dragstart', (e) => { if (!e.target.closest('.drag-handle')) { e.preventDefault(); } }, { capture: true }); // === GitLab Events === if (els.gitlabTplSelect) els.gitlabTplSelect.addEventListener('change', async (e) => { const p = e.target.value; if (!p) return; try { els.gitlabTplStatus && (els.gitlabTplStatus.textContent = "GitLab: lade Datei…"); const y = await fetchGitlabFileRaw(p); loadYAMLText(y); els.gitlabTplStatus && (els.gitlabTplStatus.textContent = `GitLab: geladen – ${p.split('/').pop()}`); } catch (err) { els.gitlabTplStatus && (els.gitlabTplStatus.textContent = "GitLab: Fehler"); console.error("[GitLab] raw failed:", err); alert("Vorlage aus GitLab konnte nicht geladen werden: " + err.message); } }); if (els.btnGitlabReload) els.btnGitlabReload.addEventListener('click', (e) => { e.preventDefault(); populateGitlabDropdown(); }); // === Drag & Drop (Rows verschieben, inkl. Gruppen) === let dragIndex = -1; // ===== Funktion: rowIndexOf ===== // Zweck: siehe Inline-Kommentare im Funktionskörper. function rowIndexOf(target) { const tr = target.closest('tr'); if (!tr) return -1; return [...els.stepsTableBody.children].indexOf(tr); } els.stepsTableBody && els.stepsTableBody.addEventListener('dragstart', (e) => { if (!e.target.closest('.drag-handle')) { e.preventDefault(); return; } const i = rowIndexOf(e.target); if (i < 0) return; dragIndex = i; e.dataTransfer?.setData('text/plain', String(i)); if (e.dataTransfer) e.dataTransfer.effectAllowed = 'move'; const row = e.target.closest('tr') || e.target; e.dataTransfer?.setDragImage?.(row, 16, 16); row.classList.add('dragging'); }); els.stepsTableBody && els.stepsTableBody.addEventListener('dragend', (e) => { const tr = e.target.closest('tr'); if (tr) tr.classList.remove('dragging'); [...els.stepsTableBody.children].forEach(r => { r.classList.remove('drag-over'); r.removeAttribute('data-pos'); }); dragIndex = -1; }); els.stepsTableBody && els.stepsTableBody.addEventListener('dragover', (e) => { e.preventDefault(); const tr = e.target.closest('tr'); if (!tr) return; [...els.stepsTableBody.children].forEach(r => { r.classList.remove('drag-over'); r.removeAttribute('data-pos'); }); tr.classList.add('drag-over'); const rect = tr.getBoundingClientRect(); const pos = (e.clientY - rect.top) < rect.height / 2 ? 'before' : 'after'; tr.setAttribute('data-pos', pos); }); els.stepsTableBody && els.stepsTableBody.addEventListener('drop', (e) => { e.preventDefault(); captureEditsIntoTemplate(); const from = dragIndex >= 0 ? dragIndex : parseInt(e.dataTransfer?.getData('text/plain') || '-1', 10); const tr = e.target.closest('tr'); if (from < 0 || !tr) return; const toBase = rowIndexOf(tr); const pos = tr.getAttribute('data-pos') || 'after'; let to = toBase + (pos === 'after' ? 1 : 0); if (to === from || to - 1 === from) { renderSteps(template.steps); recomputeGroupStyles(); return; } // Element verschieben const item = template.steps.splice(from, 1)[0]; if (to > from) to--; // nach dem Entfernen verschiebt sich Index template.steps.splice(to, 0, item); ensureRenumberAndRender(); }); document.getElementById('stepsTable')?.addEventListener('change', (e) => { if (e.target && e.target.matches('select.status')) { updateStatusClass(e.target); recomputeGroupStyles(); } }); els.btnExportMD && els.btnExportMD.addEventListener('click', exportMD); els.btnExportCSV && els.btnExportCSV.addEventListener('click', exportCSV); els.btnPrintPDF && els.btnPrintPDF.addEventListener('click', printPDF); els.btnExportTemplateYAML && els.btnExportTemplateYAML.addEventListener('click', exportTemplateYAML); els.btnSaveJSON && els.btnSaveJSON.addEventListener('click', () => { const run = collectRun(); if (!run) return; if (!checkRequired(run)) return; const base = `qa-run-${safeName(run.module)}-${safeName(run.module_version)}-${safeName(run.pbx_version)}`; download(JSON.stringify(run, null, 2), `${base}.json`, 'application/json'); }); els.loadJSON && els.loadJSON.addEventListener('change', async (e) => { const f = e.target.files?.[0]; if (!f) return; const txt = await f.text(); try { const run = JSON.parse(txt); template = { name: run.name || '', steps: (run.steps || []).map(s => ({ 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 || '—'; els.module.value = run.module || ''; els.moduleVersion.value = run.module_version || ''; els.pbxVersion.value = run.pbx_version || ''; els.tester.value = run.tester || ''; els.docbeeUrl.value = run.docbee_url || ''; renderSteps(template.steps); recomputeGroupStyles(); if (els.statusTag) els.statusTag.textContent = 'Lauf geladen'; updateAutosave(); } catch (err) { alert('Ungültiges JSON: ' + err.message); } }); // === Bindings absichern + Busy-Fix + Mini-CSS === (function ensureBindings() { // ===== Funktion: bind ===== // Zweck: siehe Inline-Kommentare im Funktionskörper. function bind() { const b = document.getElementById('btnPushDocBee'); if (b) { b.removeAttribute('disabled'); b.classList.remove('is-busy'); b.style.pointerEvents = 'auto'; b.addEventListener('click', postToDocBee, { once: false }); } const p = document.getElementById('btnPrintPDF'); const btn = document.getElementById('btnExportAll'); if (btn) { btn.addEventListener('click', e => { e.preventDefault(); exportAll().catch(err => alert('Export error: ' + err.message)); }); console.log('Export-Button gebunden'); } else { alert('Export-Button nicht gefunden (id=btnExportAll).'); } } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', bind); else bind(); window.addEventListener('pageshow', () => { const b = document.getElementById('btnPushDocBee'); if (b) { b.removeAttribute('disabled'); b.classList.remove('is-busy'); b.style.pointerEvents = 'auto'; } }); })(); (function injectAuxCss() { const s = document.createElement('style'); s.textContent = ` .is-busy{opacity:.6;pointer-events:none} .docbee-hint{margin-top:8px;font-size:14px} .docbee-hint a{text-decoration:underline} `; document.head.appendChild(s); })(); // ===================== Ende app.js =====================