343 lines
18 KiB
JavaScript
Executable File
343 lines
18 KiB
JavaScript
Executable File
/* ITSM Frontend-Logik -- CSP-konform: kein Inline-JS, keine onclick-Attribute.
|
|
Alle zustandsaendernden Requests senden das CSRF-Token als Header. */
|
|
(function () {
|
|
"use strict";
|
|
|
|
var CSRF = (document.querySelector('meta[name="csrf"]') || {}).content || "";
|
|
|
|
function esc(s) {
|
|
return String(s == null ? "" : s)
|
|
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
|
.replace(/"/g, """).replace(/'/g, "'");
|
|
}
|
|
|
|
function post(url, params) {
|
|
var body = new URLSearchParams(params || {}).toString();
|
|
return fetch(url, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
"X-CSRF-Token": CSRF
|
|
},
|
|
body: body
|
|
}).then(function (r) { return r.json().catch(function () { return { ok: r.ok }; }); });
|
|
}
|
|
|
|
function initials(label) {
|
|
var local = String(label || "?").split("@")[0];
|
|
var parts = local.split(/[._\- ]+/).filter(Boolean);
|
|
var s = parts.slice(0, 2).map(function (p) { return p[0].toUpperCase(); }).join("");
|
|
return s || "?";
|
|
}
|
|
|
|
// ── Zeilen-Navigation + Ticket-Detail ────────────────────────────────────────
|
|
document.addEventListener("click", function (ev) {
|
|
var row = ev.target.closest(".row");
|
|
if (!row) return;
|
|
if (ev.target.closest("a, button, form, select, input, textarea")) return;
|
|
if (row.dataset.ticketId) openDetail(parseInt(row.dataset.ticketId, 10));
|
|
else if (row.dataset.href) location.href = row.dataset.href;
|
|
});
|
|
|
|
// Formulare mit Bestaetigung + Auto-Submit-Selects + Change-Typ-Anzeige
|
|
document.addEventListener("submit", function (ev) {
|
|
var f = ev.target;
|
|
if (f.dataset && f.dataset.confirm && !window.confirm(f.dataset.confirm)) {
|
|
ev.preventDefault();
|
|
}
|
|
});
|
|
document.addEventListener("change", function (ev) {
|
|
if (ev.target.matches("select[data-autosubmit]")) ev.target.form.submit();
|
|
if (ev.target.id === "new-kategorie") {
|
|
var row = document.getElementById("change-typ-row");
|
|
if (row) row.hidden = ev.target.value !== "Change";
|
|
}
|
|
if (ev.target.id === "check-all") {
|
|
document.querySelectorAll(".row-check").forEach(function (c) { c.checked = ev.target.checked; });
|
|
}
|
|
});
|
|
|
|
// ── Modal "Neues Ticket" ─────────────────────────────────────────────────────
|
|
var modal = document.getElementById("new-ticket-modal");
|
|
var openBtn = document.getElementById("open-new-ticket");
|
|
var closeBtn = document.getElementById("close-new-ticket");
|
|
if (openBtn) openBtn.addEventListener("click", function () { modal.hidden = false; });
|
|
if (closeBtn) closeBtn.addEventListener("click", function () { modal.hidden = true; });
|
|
if (modal) modal.addEventListener("click", function (ev) { if (ev.target === modal) modal.hidden = true; });
|
|
|
|
// ── Aktionen-Dropdown + Bulk-Status ──────────────────────────────────────────
|
|
var bulkBtn = document.getElementById("bulk-actions-btn");
|
|
var bulkMenu = document.getElementById("bulk-menu");
|
|
if (bulkBtn) {
|
|
bulkBtn.addEventListener("click", function (ev) {
|
|
ev.stopPropagation();
|
|
bulkMenu.hidden = !bulkMenu.hidden;
|
|
});
|
|
document.addEventListener("click", function () { bulkMenu.hidden = true; });
|
|
bulkMenu.querySelectorAll("[data-bulk-status]").forEach(function (b) {
|
|
b.addEventListener("click", function () {
|
|
var ids = Array.prototype.map.call(document.querySelectorAll(".row-check:checked"), function (c) { return c.value; });
|
|
if (!ids.length) { alert("Keine Tickets ausgewaehlt."); return; }
|
|
var status = b.dataset.bulkStatus;
|
|
var done = 0, errors = [];
|
|
var next = function () {
|
|
if (!ids.length) {
|
|
if (errors.length) alert("Teilweise fehlgeschlagen (ITIL-Statusmodell):\n" + errors.join("\n"));
|
|
location.reload();
|
|
return;
|
|
}
|
|
var id = ids.shift();
|
|
post("/tickets/" + id + "/status", { status: status }).then(function (d) {
|
|
if (!d.ok) errors.push("#" + id + ": " + (d.error || "Fehler"));
|
|
done++; next();
|
|
});
|
|
};
|
|
next();
|
|
});
|
|
});
|
|
}
|
|
|
|
// ── Detail-Panel ─────────────────────────────────────────────────────────────
|
|
var overlay = document.getElementById("detail-overlay");
|
|
var panel = document.getElementById("detail-panel");
|
|
if (overlay) overlay.addEventListener("click", closeDetail);
|
|
|
|
function openDetail(id, tab) {
|
|
fetch("/api/tickets/" + id, { headers: { "Accept": "application/json" } })
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (t) {
|
|
if (!t || t.ok === false) return;
|
|
panel.innerHTML = renderDetail(t, tab || "details");
|
|
bindDetail(t, tab || "details");
|
|
panel.classList.add("open");
|
|
overlay.classList.add("open");
|
|
});
|
|
}
|
|
|
|
function closeDetail() {
|
|
panel.classList.remove("open");
|
|
overlay.classList.remove("open");
|
|
}
|
|
|
|
function pill(text, cls) {
|
|
return '<span class="pill ' + (cls || "offen") + '">' + esc(text) + "</span>";
|
|
}
|
|
|
|
function statusCls(s) {
|
|
return { "In Bearbeitung": "inbearbeitung", "Warten": "warten", "Geloest": "geloest",
|
|
"Geschlossen": "geschlossen" }[s] || "offen";
|
|
}
|
|
function prioCls(p) {
|
|
return { "Kritisch": "krit", "Hoch": "hoch", "Niedrig": "niedrig" }[p] || "mittel";
|
|
}
|
|
|
|
function renderDetail(t, tab) {
|
|
var h = "";
|
|
h += '<div class="dt-head"><h2>' + esc(t.ticket_nr) + '</h2><button class="linklike" id="dt-close">✕</button></div>';
|
|
h += '<div class="dt-service"><span class="avatar">' + esc(initials(t.service_name || t.titel)) + '</span>';
|
|
h += '<div><b>' + esc(t.service_name || "(kein Service)") + '</b><div class="sz">' + esc(t.titel) + '</div></div></div>';
|
|
|
|
h += '<div class="dt-kv">';
|
|
h += '<span class="k">Status</span><span>' + pill(t.status, statusCls(t.status)) + '</span>';
|
|
h += '<span class="k">Prioritaet</span><span>' + pill(t.prioritaet, prioCls(t.prioritaet)) + '</span>';
|
|
h += '<span class="k">Kategorie</span><span>' + pill(t.kategorie) +
|
|
(t.kategorie === "Change" && t.change_typ ? " " + pill(t.change_typ) + " " + pill("Freigabe: " + (t.approval_status || "-")) : "") +
|
|
(t.known_error ? " " + pill("Known Error", "ueberfaellig") : "") + '</span>';
|
|
h += '<span class="k">Zugewiesen an</span><span><span class="avatar sm">' + esc(initials(t.zugewiesen_an)) + '</span> ' + esc(t.zugewiesen_an || "--") + '</span>';
|
|
h += '<span class="k">Impact / Urgency</span><span>' + esc(t.impact) + ' / ' + esc(t.urgency) + '</span>';
|
|
h += '<span class="k">Fortschritt</span><span>' + t.fortschritt + '%<div class="bar-track"><div class="bar-fill" style="width:' + t.fortschritt + '%"></div></div></span>';
|
|
if (t.problem_nr) h += '<span class="k">Problem</span><span>' + esc(t.problem_nr) + '</span>';
|
|
h += '</div>';
|
|
|
|
var tabs = [["details", "Details"], ["akt", "Aktivitaeten"], ["anh", "Anhaenge"], ["auf", "Aufgaben"]];
|
|
h += '<div class="dt-tabs">' + tabs.map(function (x) {
|
|
return '<span data-dt-tab="' + x[0] + '" class="' + (tab === x[0] ? "active" : "") + '">' + x[1] + '</span>';
|
|
}).join("") + '</div>';
|
|
|
|
if (tab === "details") {
|
|
h += '<div class="sect">Beschreibung</div><div>' + esc(t.beschreibung || "--") + '</div>';
|
|
h += '<div class="sect">Betroffene Services</div><div>' + pill(t.service_name || "(kein Service)") + '</div>';
|
|
if (t.operative) {
|
|
h += '<div class="sect">CI / Betroffene Konfigurationselemente</div><div>';
|
|
(t.cis || []).forEach(function (c) {
|
|
h += '<span class="pill agent tagpill">' + esc(c.name) +
|
|
' <button class="linklike" data-ci-unlink="' + c.id + '">x</button></span>';
|
|
});
|
|
if (!(t.cis || []).length) h += '<span class="sz">keine</span>';
|
|
if (t.cis_available && t.cis_available.length) {
|
|
h += '<div style="margin-top:6px"><select id="dt-ci">';
|
|
t.cis_available.forEach(function (c) {
|
|
h += '<option value="' + c.id + '">' + esc(c.name + " (" + c.typ + ")") + "</option>";
|
|
});
|
|
h += '</select> <button class="btn ghost mini" id="dt-ci-add">+ Verknuepfen</button></div>';
|
|
}
|
|
h += '</div>';
|
|
}
|
|
if (t.sla) {
|
|
h += '<div class="sect">SLAs</div><div class="dt-kv">';
|
|
h += '<span class="k">Antwortzeit</span><span><span class="sla-chip' + (t.sla.response_breached ? " breach" : "") + '">' + esc(t.sla.response_label) + '</span></span>';
|
|
h += '<span class="k">Loesungszeit</span><span><span class="sla-chip' + (t.sla.resolve_breached ? " breach" : "") + '">' + esc(t.sla.resolve_label) + '</span></span>';
|
|
h += '</div>';
|
|
}
|
|
if (t.operative && t.kategorie !== "Problem" && t.problems && t.problems.length) {
|
|
h += '<div class="sect">Mit Problem verknuepfen</div><div>';
|
|
h += '<select id="dt-problem"><option value="">(keine Verknuepfung)</option>';
|
|
t.problems.forEach(function (p) {
|
|
var sel = t.problem_id === p.id ? " selected" : "";
|
|
h += '<option value="' + p.id + '"' + sel + ">" + esc(p.nr + " " + p.titel) + "</option>";
|
|
});
|
|
h += '</select> <button class="btn ghost mini" id="dt-problem-save">Speichern</button></div>';
|
|
}
|
|
if (t.operative && t.kategorie === "Problem") {
|
|
h += '<div class="sect">Problem Management</div><div>';
|
|
h += '<button class="btn ghost mini" data-known-error="' + (t.known_error ? "0" : "1") + '">' +
|
|
(t.known_error ? "Known-Error entfernen" : "Als Known Error markieren") + "</button></div>";
|
|
}
|
|
if (t.can_approve) {
|
|
h += '<div class="sect">Change-Freigabe (CAB)</div><div>';
|
|
h += '<button class="btn mini" data-approve="Genehmigt">Genehmigen</button> ';
|
|
h += '<button class="btn ghost mini" data-approve="Abgelehnt">Ablehnen</button></div>';
|
|
}
|
|
if (t.can_repo_edit) {
|
|
h += '<div class="sect">Repo-Datei bearbeiten (Forge, dokumentationspflichtig)</div>';
|
|
h += '<div class="panel">';
|
|
h += '<div class="grid-2"><input type="text" id="repo-name" placeholder="Repo (z.B. demo)">';
|
|
h += '<input type="text" id="repo-branch" placeholder="Branch" value="main"></div>';
|
|
h += '<div class="formrow"><input type="text" id="repo-path" placeholder="Dateipfad (z.B. README.md)"></div>';
|
|
h += '<button class="btn ghost mini" id="repo-load">Datei laden</button>';
|
|
h += '<div id="repo-status" class="sz" style="margin:6px 0"></div>';
|
|
h += '<textarea id="repo-content" rows="8" class="mono" hidden></textarea>';
|
|
h += '<input type="hidden" id="repo-sha">';
|
|
h += '<div class="formrow" id="repo-msg-row" hidden><input type="text" id="repo-msg" placeholder="Commit-Nachricht"></div>';
|
|
h += '<button class="btn mini" id="repo-save" hidden>Speichern (committen)</button>';
|
|
h += '</div>';
|
|
}
|
|
} else if (tab === "akt") {
|
|
h += '<div class="sect">Zeitleiste</div>';
|
|
h += ((t.timeline || []).map(function (e) {
|
|
return '<div class="tl-item"><b>' + esc(e.akteur || "") + '</b><div class="sz">' +
|
|
esc(e.zeit) + "</div><div>" + esc(e.text || "") + "</div></div>";
|
|
}).join("")) || '<div class="sz">Keine Ereignisse.</div>';
|
|
} else {
|
|
h += '<div class="sz" style="margin-top:10px">' +
|
|
(tab === "anh" ? "Anhaenge werden in einer spaeteren Ausbaustufe unterstuetzt." :
|
|
"Aufgaben werden in einer spaeteren Ausbaustufe unterstuetzt.") + '</div>';
|
|
}
|
|
|
|
h += '<div class="dt-footer">';
|
|
if (t.operative && t.allowed_next && t.allowed_next.length) {
|
|
h += '<select id="dt-status"><option value="">Status aendern ...</option>' +
|
|
t.allowed_next.map(function (s) { return '<option value="' + esc(s) + '">' + esc(s) + '</option>'; }).join("") +
|
|
'</select>';
|
|
}
|
|
h += '<button class="btn" id="dt-comment-btn">Kommentar hinzufuegen</button>';
|
|
h += '</div>';
|
|
h += '<div id="dt-comment-box" hidden style="margin-top:8px">';
|
|
h += '<textarea id="dt-comment" rows="2" placeholder="Kommentar (Worklog) ..."></textarea>';
|
|
h += '<div style="margin-top:6px"><button class="btn ghost mini" id="dt-comment-save">Speichern</button></div></div>';
|
|
return h;
|
|
}
|
|
|
|
function bindDetail(t, tab) {
|
|
var id = t.id;
|
|
panel.querySelectorAll("[data-dt-tab]").forEach(function (el) {
|
|
el.addEventListener("click", function () { openDetail(id, el.dataset.dtTab); });
|
|
});
|
|
var statusSel = panel.querySelector("#dt-status");
|
|
if (statusSel) statusSel.addEventListener("change", function () {
|
|
if (!statusSel.value) return;
|
|
post("/tickets/" + id + "/status", { status: statusSel.value }).then(function (d) {
|
|
if (d.ok) openDetail(id, tab); else alert(d.error || "Fehler");
|
|
});
|
|
});
|
|
panel.querySelectorAll("[data-approve]").forEach(function (b) {
|
|
b.addEventListener("click", function () {
|
|
post("/tickets/" + id + "/approval", { decision: b.dataset.approve }).then(function (d) {
|
|
if (d.ok) openDetail(id, tab); else alert(d.error || "Fehler");
|
|
});
|
|
});
|
|
});
|
|
panel.querySelectorAll("[data-known-error]").forEach(function (b) {
|
|
b.addEventListener("click", function () {
|
|
post("/tickets/" + id + "/known-error", { known_error: b.dataset.knownError }).then(function (d) {
|
|
if (d.ok) openDetail(id, tab); else alert(d.error || "Fehler");
|
|
});
|
|
});
|
|
});
|
|
panel.querySelectorAll("[data-ci-unlink]").forEach(function (b) {
|
|
b.addEventListener("click", function () {
|
|
post("/tickets/" + id + "/ci-unlink", { ci_id: b.dataset.ciUnlink }).then(function () { openDetail(id, tab); });
|
|
});
|
|
});
|
|
var ciAdd = panel.querySelector("#dt-ci-add");
|
|
if (ciAdd) ciAdd.addEventListener("click", function () {
|
|
post("/tickets/" + id + "/ci-link", { ci_id: panel.querySelector("#dt-ci").value })
|
|
.then(function () { openDetail(id, tab); });
|
|
});
|
|
var probSave = panel.querySelector("#dt-problem-save");
|
|
if (probSave) probSave.addEventListener("click", function () {
|
|
post("/tickets/" + id + "/problem-link", { problem_id: panel.querySelector("#dt-problem").value })
|
|
.then(function (d) { if (d.ok) openDetail(id, tab); else alert(d.error || "Fehler"); });
|
|
});
|
|
var commentBtn = panel.querySelector("#dt-comment-btn");
|
|
if (commentBtn) commentBtn.addEventListener("click", function () {
|
|
panel.querySelector("#dt-comment-box").hidden = false;
|
|
panel.querySelector("#dt-comment").focus();
|
|
});
|
|
var commentSave = panel.querySelector("#dt-comment-save");
|
|
if (commentSave) commentSave.addEventListener("click", function () {
|
|
var text = panel.querySelector("#dt-comment").value.trim();
|
|
if (!text) return;
|
|
post("/tickets/" + id + "/comment", { text: text }).then(function (d) {
|
|
if (d.ok) openDetail(id, "akt"); else alert(d.error || "Fehler");
|
|
});
|
|
});
|
|
var close = panel.querySelector("#dt-close");
|
|
if (close) close.addEventListener("click", closeDetail);
|
|
|
|
var repoLoad = panel.querySelector("#repo-load");
|
|
if (repoLoad) {
|
|
repoLoad.addEventListener("click", function () {
|
|
var repo = panel.querySelector("#repo-name").value.trim();
|
|
var path = panel.querySelector("#repo-path").value.trim();
|
|
var branch = panel.querySelector("#repo-branch").value.trim() || "main";
|
|
var status = panel.querySelector("#repo-status");
|
|
if (!repo || !path) { status.textContent = "Repo und Dateipfad angeben."; return; }
|
|
status.textContent = "Laedt...";
|
|
fetch("/api/tickets/" + id + "/repo-file?repo=" + encodeURIComponent(repo) +
|
|
"&path=" + encodeURIComponent(path) + "&branch=" + encodeURIComponent(branch))
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (d) {
|
|
if (!d.ok) { status.textContent = "Fehler: " + d.error; return; }
|
|
status.textContent = "Geladen (sha " + d.sha.slice(0, 10) + ").";
|
|
var ta = panel.querySelector("#repo-content");
|
|
ta.value = d.content;
|
|
ta.hidden = false;
|
|
panel.querySelector("#repo-sha").value = d.sha;
|
|
panel.querySelector("#repo-msg-row").hidden = false;
|
|
panel.querySelector("#repo-save").hidden = false;
|
|
})
|
|
.catch(function () { status.textContent = "Fehler beim Laden."; });
|
|
});
|
|
panel.querySelector("#repo-save").addEventListener("click", function () {
|
|
var status = panel.querySelector("#repo-status");
|
|
var message = panel.querySelector("#repo-msg").value.trim();
|
|
if (!message) { status.textContent = "Bitte eine Commit-Nachricht angeben."; return; }
|
|
status.textContent = "Speichert...";
|
|
post("/tickets/" + id + "/repo-edit", {
|
|
repo: panel.querySelector("#repo-name").value.trim(),
|
|
path: panel.querySelector("#repo-path").value.trim(),
|
|
branch: panel.querySelector("#repo-branch").value.trim() || "main",
|
|
sha: panel.querySelector("#repo-sha").value,
|
|
content: panel.querySelector("#repo-content").value,
|
|
message: message
|
|
}).then(function (d) {
|
|
if (!d.ok) { status.textContent = "Fehler: " + d.error; return; }
|
|
status.textContent = "Gespeichert -- Commit " + d.commit.slice(0, 10) + ", im Worklog dokumentiert.";
|
|
openDetail(id, "akt");
|
|
}).catch(function () { status.textContent = "Fehler beim Speichern."; });
|
|
});
|
|
}
|
|
}
|
|
})();
|