Internal Flask app that monitors the epi-helpdesk.zammad.com tickets via the Zammad API, deployable as a systemd service behind Apache under /episupport. Features: - Poller: session-login API sync, snapshot diff to detect agent changes (status, owner, priority, closures, new replies); email notifications - Dashboard: ticket overview with sorting, "not closed" filter and highlighting of tickets with unconfirmed changes; per-item and bulk "mark as read" - Analytics: KPIs, response/resolution times, monthly SVG trend chart, distributions (status/priority/group/agent), Excel export - Conversation view: full per-ticket thread cached in the DB (avoids proxy timeouts), inline image attachments, safe HTML-to-text rendering - Write actions: reply to tickets and create new tickets with a confirmation step and attachment upload (incl. clipboard paste); self-triggered actions are synced back so they aren't flagged as changes - Local timezone display (Europe/Berlin) for all timestamps Includes deploy tooling: setup.sh, systemd units and Apache config. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
79 lines
2.6 KiB
JavaScript
79 lines
2.6 KiB
JavaScript
// Anhänge: Dateiauswahl, Einfügen aus Zwischenablage (Strg+V) und Drag & Drop.
|
||
(function () {
|
||
function initAttach(form) {
|
||
var input = form.querySelector(".attach-input");
|
||
var list = form.querySelector(".attach-list");
|
||
if (!input) return;
|
||
var dt = new DataTransfer();
|
||
var pasteCount = 0;
|
||
|
||
function refresh() {
|
||
input.files = dt.files;
|
||
list.innerHTML = "";
|
||
Array.prototype.forEach.call(dt.files, function (f, i) {
|
||
var chip = document.createElement("span");
|
||
chip.className = "attach-chip";
|
||
chip.textContent = (f.name || "Datei") + " ";
|
||
var x = document.createElement("button");
|
||
x.type = "button";
|
||
x.textContent = "×";
|
||
x.title = "Entfernen";
|
||
x.addEventListener("click", function () {
|
||
var keep = new DataTransfer();
|
||
Array.prototype.forEach.call(dt.files, function (ff, j) {
|
||
if (j !== i) keep.items.add(ff);
|
||
});
|
||
dt = keep;
|
||
refresh();
|
||
});
|
||
chip.appendChild(x);
|
||
list.appendChild(chip);
|
||
});
|
||
}
|
||
|
||
function addFiles(files) {
|
||
var added = false;
|
||
Array.prototype.forEach.call(files, function (f) {
|
||
if (f) { dt.items.add(f); added = true; }
|
||
});
|
||
if (added) refresh();
|
||
}
|
||
|
||
input.addEventListener("change", function () {
|
||
addFiles(input.files);
|
||
});
|
||
|
||
form.addEventListener("paste", function (e) {
|
||
var items = e.clipboardData && e.clipboardData.items;
|
||
if (!items) return;
|
||
var imgs = [];
|
||
for (var i = 0; i < items.length; i++) {
|
||
if (items[i].kind === "file") {
|
||
var f = items[i].getAsFile();
|
||
if (f) {
|
||
if (!f.name || f.name === "image.png") {
|
||
pasteCount++;
|
||
var ext = (f.type && f.type.split("/")[1]) || "png";
|
||
f = new File([f], "einfuegung-" + pasteCount + "." + ext, { type: f.type });
|
||
}
|
||
imgs.push(f);
|
||
}
|
||
}
|
||
}
|
||
if (imgs.length) { e.preventDefault(); addFiles(imgs); }
|
||
});
|
||
|
||
["dragover", "dragenter"].forEach(function (ev) {
|
||
form.addEventListener(ev, function (e) { e.preventDefault(); form.classList.add("dragging"); });
|
||
});
|
||
["dragleave", "drop"].forEach(function (ev) {
|
||
form.addEventListener(ev, function (e) { e.preventDefault(); form.classList.remove("dragging"); });
|
||
});
|
||
form.addEventListener("drop", function (e) {
|
||
if (e.dataTransfer && e.dataTransfer.files) addFiles(e.dataTransfer.files);
|
||
});
|
||
}
|
||
|
||
document.querySelectorAll("form.attach-form").forEach(initAttach);
|
||
})();
|