271 lines
12 KiB
JavaScript
Executable File
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, "&").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 }; }); });
|
|
}
|
|
|
|
// ── 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."; });
|
|
});
|
|
}
|
|
}
|
|
})();
|