ITSM/static/app.js

271 lines
12 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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
}
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 }; }); });
}
// ── Zeilen-Navigation + Ticket-Detail ────────────────────────────────────────
document.addEventListener("click", function (ev) {
var row = ev.target.closest("tr.row");
if (!row) return;
if (ev.target.closest("a, button, form, select, input")) return;
if (row.dataset.ticketId) openDetail(parseInt(row.dataset.ticketId, 10));
else if (row.dataset.href) location.href = row.dataset.href;
});
// Formulare mit Bestaetigung (z. B. Loeschen) und Auto-Submit-Selects.
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";
}
});
var overlay = document.getElementById("detail-overlay");
var panel = document.getElementById("detail-panel");
if (overlay) overlay.addEventListener("click", closeDetail);
function openDetail(id) {
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);
bindDetail(t);
panel.classList.add("open");
overlay.classList.add("open");
});
}
function closeDetail() {
panel.classList.remove("open");
overlay.classList.remove("open");
}
function pill(text) {
return '<span class="pill">' + esc(text) + "</span> ";
}
function renderDetail(t) {
var h = "";
h += "<h2>" + esc(t.ticket_nr) + "</h2>";
h += '<div class="sz" style="margin-bottom:10px">' + esc(t.titel) + "</div>";
h += "<div>" + pill(t.status) + pill(t.prioritaet) + pill(t.kategorie);
h += pill("Impact: " + t.impact) + pill("Urgency: " + t.urgency);
if (t.kategorie === "Change" && t.change_typ) h += pill(t.change_typ) + pill("Freigabe: " + (t.approval_status || "-"));
if (t.known_error) h += pill("Known Error");
h += "</div>";
h += '<div class="sect">Beschreibung</div><div>' + esc(t.beschreibung || "") + "</div>";
h += '<div class="sect">Service</div><div>' + esc(t.service_name || "(kein Service)") + "</div>";
if (t.sla) {
h += '<div class="sect">SLA</div><div class="sz">';
h += "Antwort bis: " + esc(t.sla.response_due || "--");
h += " · Loesung bis: " + esc(t.sla.resolve_due || "--");
if (t.sla.overdue) h += ' · <span class="pill ueberfaellig">Ueberfaellig</span>';
h += "</div>";
}
if (t.problem_nr) {
h += '<div class="sect">Problem</div><div class="sz">Verknuepft mit ' + esc(t.problem_nr) + "</div>";
}
if (t.operative && t.allowed_next && t.allowed_next.length) {
h += '<div class="sect">Status aendern (erlaubte ITIL-Uebergaenge)</div><div class="btnrow">';
t.allowed_next.forEach(function (s) {
h += '<button class="btn ghost" data-status="' + esc(s) + '">' + esc(s) + "</button>";
});
h += "</div>";
}
if (t.can_approve) {
h += '<div class="sect">Change-Freigabe (CAB)</div><div class="btnrow">';
h += '<button class="btn" data-approve="Genehmigt">Genehmigen</button>';
h += '<button class="btn ghost" data-approve="Abgelehnt">Ablehnen</button></div>';
}
if (t.operative && t.kategorie === "Problem") {
h += '<div class="sect">Problem Management</div><div class="btnrow">';
h += '<button class="btn ghost" data-known-error="' + (t.known_error ? "0" : "1") + '">' +
(t.known_error ? "Known-Error-Markierung entfernen" : "Als Known Error markieren") + "</button></div>";
}
if (t.operative && t.kategorie !== "Problem" && t.problems && t.problems.length) {
h += '<div class="sect">Mit Problem verknuepfen</div><div class="btnrow">';
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" id="dt-problem-save">Speichern</button></div>';
}
if (t.operative) {
h += '<div class="sect">Configuration Items (CMDB)</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_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" id="dt-ci-add">Verknuepfen</button></div>';
}
h += "</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" 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" id="repo-save" hidden>Speichern (im Repo committen)</button>';
h += "</div>";
} else if (t.operative && t.kategorie === "Change") {
h += '<div class="sz" style="margin-top:10px">Repo-Bearbeitung: erfordert Rolle Change Manager/Administrator und einen freigegebenen Change.</div>';
}
h += '<div class="sect">Kommentar (Worklog)</div>';
h += '<textarea id="dt-comment" rows="2" placeholder="Kommentar hinzufuegen ..."></textarea>';
h += '<div style="margin-top:6px"><button class="btn ghost" id="dt-comment-save">Hinzufuegen</button></div>';
h += '<div class="sect">Zeitleiste</div>';
var tl = (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>';
h += tl;
h += '<div style="margin-top:16px"><button class="btn ghost" id="dt-close">Schliessen</button></div>';
return h;
}
function bindDetail(t) {
var id = t.id;
panel.querySelectorAll("[data-status]").forEach(function (b) {
b.addEventListener("click", function () {
post("/tickets/" + id + "/status", { status: b.dataset.status }).then(function (d) {
if (d.ok) openDetail(id); 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); 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); 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); });
});
});
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); });
});
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); else alert(d.error || "Fehler"); });
});
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); else alert(d.error || "Fehler");
});
});
var close = panel.querySelector("#dt-close");
if (close) close.addEventListener("click", closeDetail);
// Repo-Bearbeitung (nur wenn gerendert)
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);
}).catch(function () { status.textContent = "Fehler beim Speichern."; });
});
}
}
})();