Initial commit: AegisSight-Monitor-Verwaltung
Dieser Commit ist enthalten in:
561
src/static/js/app.js
Normale Datei
561
src/static/js/app.js
Normale Datei
@@ -0,0 +1,561 @@
|
||||
/* Verwaltungsportal - Frontend Logic */
|
||||
"use strict";
|
||||
|
||||
const API = {
|
||||
token: localStorage.getItem("token"),
|
||||
|
||||
async request(path, opts = {}) {
|
||||
const headers = { "Content-Type": "application/json" };
|
||||
if (this.token) headers["Authorization"] = `Bearer ${this.token}`;
|
||||
const res = await fetch(path, { ...opts, headers });
|
||||
if (res.status === 401) {
|
||||
localStorage.removeItem("token");
|
||||
localStorage.removeItem("username");
|
||||
window.location.href = "/";
|
||||
return;
|
||||
}
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
throw new Error(data.detail || `Fehler ${res.status}`);
|
||||
}
|
||||
if (res.status === 204) return null;
|
||||
return res.json();
|
||||
},
|
||||
|
||||
get(path) { return this.request(path); },
|
||||
post(path, body) { return this.request(path, { method: "POST", body: JSON.stringify(body) }); },
|
||||
put(path, body) { return this.request(path, { method: "PUT", body: body ? JSON.stringify(body) : undefined }); },
|
||||
del(path) { return this.request(path, { method: "DELETE" }); },
|
||||
};
|
||||
|
||||
// --- State ---
|
||||
let currentOrgId = null;
|
||||
let orgsCache = [];
|
||||
|
||||
// --- Init ---
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
if (!API.token) { window.location.href = "/"; return; }
|
||||
|
||||
document.getElementById("headerUser").textContent = localStorage.getItem("username") || "";
|
||||
document.getElementById("logoutBtn").addEventListener("click", logout);
|
||||
|
||||
setupNavTabs();
|
||||
setupOrgDetailTabs();
|
||||
setupForms();
|
||||
loadDashboard();
|
||||
loadOrgs();
|
||||
});
|
||||
|
||||
function logout() {
|
||||
localStorage.removeItem("token");
|
||||
localStorage.removeItem("username");
|
||||
window.location.href = "/";
|
||||
}
|
||||
|
||||
// --- Navigation ---
|
||||
function setupNavTabs() {
|
||||
document.querySelectorAll(".nav-tabs:not(#orgDetailTabs) .nav-tab").forEach(tab => {
|
||||
tab.addEventListener("click", () => {
|
||||
const section = tab.dataset.section;
|
||||
document.querySelectorAll(".nav-tabs:not(#orgDetailTabs) .nav-tab").forEach(t => t.classList.remove("active"));
|
||||
tab.classList.add("active");
|
||||
document.querySelectorAll(".app-content > .section").forEach(s => s.classList.remove("active"));
|
||||
document.getElementById(`sec-${section}`).classList.add("active");
|
||||
|
||||
if (section === "licenses") loadExpiringLicenses();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setupOrgDetailTabs() {
|
||||
document.querySelectorAll("#orgDetailTabs .nav-tab").forEach(tab => {
|
||||
tab.addEventListener("click", () => {
|
||||
const subtab = tab.dataset.subtab;
|
||||
document.querySelectorAll("#orgDetailTabs .nav-tab").forEach(t => t.classList.remove("active"));
|
||||
tab.classList.add("active");
|
||||
document.querySelectorAll("#orgDetail > .section").forEach(s => s.classList.remove("active"));
|
||||
document.getElementById(`sub-${subtab}`).classList.add("active");
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById("orgBackBtn").addEventListener("click", () => {
|
||||
document.getElementById("orgListView").style.display = "";
|
||||
document.getElementById("orgDetail").classList.remove("active");
|
||||
currentOrgId = null;
|
||||
});
|
||||
}
|
||||
|
||||
// --- Dashboard ---
|
||||
async function loadDashboard() {
|
||||
try {
|
||||
const stats = await API.get("/api/dashboard/stats");
|
||||
|
||||
document.getElementById("statsGrid").innerHTML = `
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Organisationen</div>
|
||||
<div class="stat-value">${stats.organizations.total}</div>
|
||||
<div class="stat-sub">${stats.organizations.active} aktiv</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Nutzer</div>
|
||||
<div class="stat-value">${stats.users.total}</div>
|
||||
<div class="stat-sub">${stats.users.active} aktiv</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Aktive Lizenzen</div>
|
||||
<div class="stat-value">${stats.licenses.active}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Vorfaelle</div>
|
||||
<div class="stat-value">${stats.incidents.total}</div>
|
||||
<div class="stat-sub">${stats.incidents.active} aktiv</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Expiring licenses
|
||||
const expList = document.getElementById("expiringList");
|
||||
if (stats.expiring_licenses.length === 0) {
|
||||
expList.innerHTML = '<li class="text-muted" style="padding: 8px 0;">Keine ablaufenden Lizenzen</li>';
|
||||
} else {
|
||||
expList.innerHTML = stats.expiring_licenses.map(l => `
|
||||
<li class="expiring-item">
|
||||
<span>${esc(l.org_name)} <span class="badge badge-${l.license_type}">${l.license_type}</span></span>
|
||||
<span class="expiring-date">${formatDate(l.valid_until)}</span>
|
||||
</li>
|
||||
`).join("");
|
||||
}
|
||||
|
||||
// Recent activity
|
||||
const actEl = document.getElementById("recentActivity");
|
||||
if (stats.recent_activity.length === 0) {
|
||||
actEl.innerHTML = '<div class="text-muted">Keine Aktivitaet</div>';
|
||||
} else {
|
||||
actEl.innerHTML = stats.recent_activity.map(a => `
|
||||
<div class="activity-item">
|
||||
<div class="activity-icon ${a.type}">${a.type === "org" ? "O" : "U"}</div>
|
||||
<div>
|
||||
<div>${esc(a.label)}</div>
|
||||
<div class="text-muted" style="font-size: 12px;">${formatDate(a.created_at)}</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join("");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Dashboard laden fehlgeschlagen:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Organizations ---
|
||||
async function loadOrgs() {
|
||||
try {
|
||||
orgsCache = await API.get("/api/orgs");
|
||||
renderOrgTable(orgsCache);
|
||||
} catch (err) {
|
||||
console.error("Orgs laden fehlgeschlagen:", err);
|
||||
}
|
||||
}
|
||||
|
||||
function renderOrgTable(orgs) {
|
||||
const tbody = document.getElementById("orgTable");
|
||||
if (orgs.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="6" class="text-muted">Keine Organisationen</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = orgs.map(o => `
|
||||
<tr>
|
||||
<td><a href="#" class="text-accent" style="text-decoration: none;" onclick="openOrg(${o.id}); return false;">${esc(o.name)}</a></td>
|
||||
<td class="text-secondary">${esc(o.slug)}</td>
|
||||
<td>${o.user_count}</td>
|
||||
<td><span class="badge badge-${o.license_type || 'none'}">${o.license_type || "Keine"}</span></td>
|
||||
<td><span class="badge badge-${o.is_active ? 'active' : 'inactive'}">${o.is_active ? "Aktiv" : "Inaktiv"}</span></td>
|
||||
<td>
|
||||
<button class="btn btn-secondary btn-small" onclick="openOrg(${o.id})">Details</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join("");
|
||||
}
|
||||
|
||||
// Search filter
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const searchEl = document.getElementById("orgSearch");
|
||||
if (searchEl) {
|
||||
searchEl.addEventListener("input", () => {
|
||||
const q = searchEl.value.toLowerCase();
|
||||
const filtered = orgsCache.filter(o =>
|
||||
o.name.toLowerCase().includes(q) || o.slug.toLowerCase().includes(q)
|
||||
);
|
||||
renderOrgTable(filtered);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// --- Open Org Detail ---
|
||||
async function openOrg(orgId) {
|
||||
currentOrgId = orgId;
|
||||
document.getElementById("orgListView").style.display = "none";
|
||||
document.getElementById("orgDetail").classList.add("active");
|
||||
|
||||
// Reset to users tab
|
||||
document.querySelectorAll("#orgDetailTabs .nav-tab").forEach(t => t.classList.remove("active"));
|
||||
document.querySelector('#orgDetailTabs .nav-tab[data-subtab="users"]').classList.add("active");
|
||||
document.querySelectorAll("#orgDetail > .section").forEach(s => s.classList.remove("active"));
|
||||
document.getElementById("sub-users").classList.add("active");
|
||||
|
||||
try {
|
||||
const org = await API.get(`/api/orgs/${orgId}`);
|
||||
document.getElementById("orgDetailHeader").innerHTML = `
|
||||
<h2>${esc(org.name)} <span class="badge badge-${org.is_active ? 'active' : 'inactive'}" style="font-size: 12px; vertical-align: middle;">${org.is_active ? "Aktiv" : "Inaktiv"}</span></h2>
|
||||
<div class="text-secondary mt-8">Slug: ${esc(org.slug)} | Nutzer: ${org.user_count} | Lizenz: ${org.license_type || "Keine"}</div>
|
||||
`;
|
||||
|
||||
document.getElementById("editOrgName").value = org.name;
|
||||
document.getElementById("editOrgActive").value = org.is_active ? "true" : "false";
|
||||
|
||||
loadOrgUsers(orgId);
|
||||
loadOrgLicenses(orgId);
|
||||
} catch (err) {
|
||||
console.error("Org laden fehlgeschlagen:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Org Users ---
|
||||
async function loadOrgUsers(orgId) {
|
||||
try {
|
||||
const users = await API.get(`/api/users?org_id=${orgId}`);
|
||||
const licenses = await API.get(`/api/licenses?org_id=${orgId}`);
|
||||
const activeLic = licenses.find(l => l.status === "active");
|
||||
const activeUsers = users.filter(u => u.is_active).length;
|
||||
|
||||
document.getElementById("userLimitInfo").textContent = activeLic
|
||||
? `${activeUsers} / ${activeLic.max_users} Nutzer`
|
||||
: "Keine aktive Lizenz";
|
||||
|
||||
const tbody = document.getElementById("userTable");
|
||||
if (users.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="5" class="text-muted">Keine Nutzer</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = users.map(u => `
|
||||
<tr>
|
||||
<td>${esc(u.email)}</td>
|
||||
<td>${esc(u.username)}</td>
|
||||
<td>
|
||||
<select class="btn btn-secondary btn-small" onchange="changeRole(${u.id}, this.value)" style="padding: 4px 8px;">
|
||||
<option value="member" ${u.role === "member" ? "selected" : ""}>Mitglied</option>
|
||||
<option value="org_admin" ${u.role === "org_admin" ? "selected" : ""}>Org-Admin</option>
|
||||
</select>
|
||||
</td>
|
||||
<td><span class="badge badge-${u.is_active ? 'active' : 'inactive'}">${u.is_active ? "Aktiv" : "Inaktiv"}</span></td>
|
||||
<td>
|
||||
${u.is_active
|
||||
? `<button class="btn btn-secondary btn-small" onclick="toggleUser(${u.id}, false)">Deaktivieren</button>`
|
||||
: `<button class="btn btn-success btn-small" onclick="toggleUser(${u.id}, true)">Aktivieren</button>`
|
||||
}
|
||||
<button class="btn btn-danger btn-small" onclick="confirmDeleteUser(${u.id}, '${esc(u.email)}')">Loeschen</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join("");
|
||||
} catch (err) {
|
||||
console.error("Nutzer laden fehlgeschlagen:", err);
|
||||
}
|
||||
}
|
||||
|
||||
async function changeRole(userId, role) {
|
||||
try {
|
||||
await API.put(`/api/users/${userId}/role?role=${role}`);
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
if (currentOrgId) loadOrgUsers(currentOrgId);
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleUser(userId, activate) {
|
||||
try {
|
||||
await API.put(`/api/users/${userId}/${activate ? "activate" : "deactivate"}`);
|
||||
if (currentOrgId) loadOrgUsers(currentOrgId);
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDeleteUser(userId, email) {
|
||||
showConfirm(
|
||||
"Nutzer loeschen",
|
||||
`Soll der Nutzer "${email}" endgueltig geloescht werden?`,
|
||||
async () => {
|
||||
try {
|
||||
await API.del(`/api/users/${userId}`);
|
||||
if (currentOrgId) loadOrgUsers(currentOrgId);
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// --- Org Licenses ---
|
||||
async function loadOrgLicenses(orgId) {
|
||||
try {
|
||||
const licenses = await API.get(`/api/licenses?org_id=${orgId}`);
|
||||
const tbody = document.getElementById("licenseTable");
|
||||
if (licenses.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="6" class="text-muted">Keine Lizenzen</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = licenses.map(l => `
|
||||
<tr>
|
||||
<td><span class="badge badge-${l.license_type}">${l.license_type}</span></td>
|
||||
<td>${l.max_users}</td>
|
||||
<td>${formatDate(l.valid_from)}</td>
|
||||
<td>${l.valid_until ? formatDate(l.valid_until) : "Unbegrenzt"}</td>
|
||||
<td><span class="badge badge-${l.status}">${l.status}</span></td>
|
||||
<td>
|
||||
${l.status === "active" ? `
|
||||
<button class="btn btn-secondary btn-small" onclick="extendLicense(${l.id})">Verlaengern</button>
|
||||
<button class="btn btn-danger btn-small" onclick="confirmRevokeLicense(${l.id})">Widerrufen</button>
|
||||
` : ""}
|
||||
</td>
|
||||
</tr>
|
||||
`).join("");
|
||||
} catch (err) {
|
||||
console.error("Lizenzen laden fehlgeschlagen:", err);
|
||||
}
|
||||
}
|
||||
|
||||
async function extendLicense(licId) {
|
||||
const days = prompt("Um wie viele Tage verlaengern?", "365");
|
||||
if (!days) return;
|
||||
try {
|
||||
await API.put(`/api/licenses/${licId}/extend?days=${parseInt(days)}`);
|
||||
if (currentOrgId) loadOrgLicenses(currentOrgId);
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function confirmRevokeLicense(licId) {
|
||||
showConfirm(
|
||||
"Lizenz widerrufen",
|
||||
"Soll die Lizenz wirklich widerrufen werden? Nutzer koennen dann nur noch lesen.",
|
||||
async () => {
|
||||
try {
|
||||
await API.put(`/api/licenses/${licId}/revoke`);
|
||||
if (currentOrgId) loadOrgLicenses(currentOrgId);
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// --- Expiring Licenses (global view) ---
|
||||
async function loadExpiringLicenses() {
|
||||
const days = document.getElementById("expiringDays").value;
|
||||
try {
|
||||
const licenses = await API.get(`/api/licenses/expiring?days=${days}`);
|
||||
const tbody = document.getElementById("expiringTable");
|
||||
if (licenses.length === 0) {
|
||||
tbody.innerHTML = `<tr><td colspan="5" class="text-muted">Keine ablaufenden Lizenzen in den naechsten ${days} Tagen</td></tr>`;
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = licenses.map(l => `
|
||||
<tr>
|
||||
<td><a href="#" class="text-accent" style="text-decoration: none;" onclick="switchToOrg(${l.organization_id}); return false;">${esc(l.org_name)}</a></td>
|
||||
<td><span class="badge badge-${l.license_type}">${l.license_type}</span></td>
|
||||
<td>${l.max_users}</td>
|
||||
<td class="text-warning">${formatDate(l.valid_until)}</td>
|
||||
<td>
|
||||
<button class="btn btn-secondary btn-small" onclick="extendLicense(${l.id})">Verlaengern</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join("");
|
||||
} catch (err) {
|
||||
console.error("Ablaufende Lizenzen laden fehlgeschlagen:", err);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const sel = document.getElementById("expiringDays");
|
||||
if (sel) sel.addEventListener("change", loadExpiringLicenses);
|
||||
});
|
||||
|
||||
function switchToOrg(orgId) {
|
||||
// Switch to orgs tab and open detail
|
||||
document.querySelectorAll(".nav-tabs:not(#orgDetailTabs) .nav-tab").forEach(t => t.classList.remove("active"));
|
||||
document.querySelector('.nav-tab[data-section="orgs"]').classList.add("active");
|
||||
document.querySelectorAll(".app-content > .section").forEach(s => s.classList.remove("active"));
|
||||
document.getElementById("sec-orgs").classList.add("active");
|
||||
openOrg(orgId);
|
||||
}
|
||||
|
||||
// --- Forms ---
|
||||
function setupForms() {
|
||||
// New Org
|
||||
document.getElementById("newOrgBtn").addEventListener("click", () => openModal("modalNewOrg"));
|
||||
document.getElementById("newOrgForm").addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const errEl = document.getElementById("newOrgError");
|
||||
errEl.style.display = "none";
|
||||
try {
|
||||
await API.post("/api/orgs", {
|
||||
name: document.getElementById("newOrgName").value,
|
||||
slug: document.getElementById("newOrgSlug").value,
|
||||
});
|
||||
closeModal("modalNewOrg");
|
||||
document.getElementById("newOrgForm").reset();
|
||||
loadOrgs();
|
||||
loadDashboard();
|
||||
} catch (err) {
|
||||
errEl.textContent = err.message;
|
||||
errEl.style.display = "block";
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-generate slug from name
|
||||
document.getElementById("newOrgName").addEventListener("input", (e) => {
|
||||
const slug = e.target.value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
||||
document.getElementById("newOrgSlug").value = slug;
|
||||
});
|
||||
|
||||
// New User
|
||||
document.getElementById("newUserBtn").addEventListener("click", () => openModal("modalNewUser"));
|
||||
document.getElementById("newUserForm").addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const errEl = document.getElementById("newUserError");
|
||||
errEl.style.display = "none";
|
||||
try {
|
||||
await API.post(`/api/users?org_id=${currentOrgId}`, {
|
||||
email: document.getElementById("newUserEmail").value,
|
||||
username: document.getElementById("newUserName").value,
|
||||
role: document.getElementById("newUserRole").value,
|
||||
});
|
||||
closeModal("modalNewUser");
|
||||
document.getElementById("newUserForm").reset();
|
||||
loadOrgUsers(currentOrgId);
|
||||
} catch (err) {
|
||||
errEl.textContent = err.message;
|
||||
errEl.style.display = "block";
|
||||
}
|
||||
});
|
||||
|
||||
// New License
|
||||
document.getElementById("newLicenseBtn").addEventListener("click", () => {
|
||||
document.getElementById("newLicenseForm").reset();
|
||||
openModal("modalNewLicense");
|
||||
});
|
||||
|
||||
document.getElementById("newLicType").addEventListener("change", (e) => {
|
||||
const durationGroup = document.getElementById("durationGroup");
|
||||
if (e.target.value === "permanent") {
|
||||
durationGroup.style.display = "none";
|
||||
} else {
|
||||
durationGroup.style.display = "";
|
||||
document.getElementById("newLicDuration").value = e.target.value === "trial" ? "14" : "365";
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("newLicenseForm").addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const errEl = document.getElementById("newLicError");
|
||||
errEl.style.display = "none";
|
||||
const licType = document.getElementById("newLicType").value;
|
||||
const body = {
|
||||
organization_id: currentOrgId,
|
||||
license_type: licType,
|
||||
max_users: parseInt(document.getElementById("newLicMaxUsers").value),
|
||||
};
|
||||
if (licType !== "permanent") {
|
||||
body.duration_days = parseInt(document.getElementById("newLicDuration").value);
|
||||
}
|
||||
try {
|
||||
await API.post("/api/licenses", body);
|
||||
closeModal("modalNewLicense");
|
||||
loadOrgLicenses(currentOrgId);
|
||||
loadDashboard();
|
||||
} catch (err) {
|
||||
errEl.textContent = err.message;
|
||||
errEl.style.display = "block";
|
||||
}
|
||||
});
|
||||
|
||||
// Org Edit
|
||||
document.getElementById("orgEditForm").addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await API.put(`/api/orgs/${currentOrgId}`, {
|
||||
name: document.getElementById("editOrgName").value,
|
||||
is_active: document.getElementById("editOrgActive").value === "true",
|
||||
});
|
||||
openOrg(currentOrgId);
|
||||
loadOrgs();
|
||||
loadDashboard();
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Delete Org
|
||||
document.getElementById("deleteOrgBtn").addEventListener("click", () => {
|
||||
showConfirm(
|
||||
"Organisation loeschen",
|
||||
"Soll die Organisation mit allen Nutzern und Lizenzen endgueltig geloescht werden? Diese Aktion kann nicht rueckgaengig gemacht werden.",
|
||||
async () => {
|
||||
try {
|
||||
await API.del(`/api/orgs/${currentOrgId}`);
|
||||
document.getElementById("orgListView").style.display = "";
|
||||
document.getElementById("orgDetail").classList.remove("active");
|
||||
currentOrgId = null;
|
||||
loadOrgs();
|
||||
loadDashboard();
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// --- Modal helpers ---
|
||||
function openModal(id) {
|
||||
document.getElementById(id).classList.add("active");
|
||||
}
|
||||
|
||||
function closeModal(id) {
|
||||
document.getElementById(id).classList.remove("active");
|
||||
}
|
||||
|
||||
// Confirm dialog
|
||||
let confirmCallback = null;
|
||||
|
||||
function showConfirm(title, text, callback) {
|
||||
document.getElementById("confirmTitle").textContent = title;
|
||||
document.getElementById("confirmText").textContent = text;
|
||||
confirmCallback = callback;
|
||||
openModal("modalConfirm");
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
document.getElementById("confirmOkBtn").addEventListener("click", async () => {
|
||||
closeModal("modalConfirm");
|
||||
if (confirmCallback) await confirmCallback();
|
||||
confirmCallback = null;
|
||||
});
|
||||
});
|
||||
|
||||
// --- Utilities ---
|
||||
function esc(str) {
|
||||
if (!str) return "";
|
||||
const div = document.createElement("div");
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function formatDate(iso) {
|
||||
if (!iso) return "-";
|
||||
try {
|
||||
const d = new Date(iso);
|
||||
return d.toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit", year: "numeric" });
|
||||
} catch {
|
||||
return iso;
|
||||
}
|
||||
}
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren