/* 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" }); }, async upload(path, formData) { const headers = {}; if (this.token) headers["Authorization"] = `Bearer ${this.token}`; const res = await fetch(path, { method: "POST", headers, body: formData }); 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}`); } return res.json(); }, }; // --- Theme (Dark = Standard, Wahl bleibt in localStorage erhalten) --- const ThemeManager = { _key: "portal_theme", init() { const theme = localStorage.getItem(this._key) === "light" ? "light" : "dark"; this._apply(theme); }, toggle() { const next = document.documentElement.getAttribute("data-theme") === "light" ? "dark" : "light"; localStorage.setItem(this._key, next); this._apply(next); }, _apply(theme) { if (theme === "light") document.documentElement.setAttribute("data-theme", "light"); else document.documentElement.removeAttribute("data-theme"); const el = document.getElementById("theme-toggle"); if (el) { el.classList.remove("dark", "light"); el.classList.add(theme); el.setAttribute("aria-checked", theme === "dark" ? "true" : "false"); } }, }; // Sprung zum (nachgeladenen) Verbrauchsrechner-Tab function openRechnerTab() { const tab = document.querySelector('.nav-tab[data-section="rechner"]'); if (tab) tab.click(); } // Tab-Wechsel per Name, nutzt die vorhandene Nav-Logik function gotoSection(name) { const tab = document.querySelector(`.nav-tab[data-section="${name}"]`); if (tab) tab.click(); } // Dashboard-Kacheln als Sprungziele verdrahten (Klick + Enter/Leertaste) function wireStatCardLinks() { document.querySelectorAll("#statsGrid .stat-card-link").forEach((card) => { const go = () => { if (card.dataset.href) window.open(card.dataset.href, "_blank", "noopener"); else if (card.dataset.goto) gotoSection(card.dataset.goto); }; card.addEventListener("click", go); card.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); go(); } }); }); } // --- State --- let currentOrgId = null; let orgsCache = []; // --- Init --- document.addEventListener("DOMContentLoaded", () => { if (!API.token) { window.location.href = "/"; return; } ThemeManager.init(); // Barrierefreiheits-Panel (a11y.js, identische Datei wie im Monitor) if (typeof A11yManager !== "undefined") A11yManager.init(); document.getElementById("headerUser").textContent = localStorage.getItem("username") || ""; document.getElementById("logoutBtn").addEventListener("click", logout); setupNavTabs(); setupOrgDetailTabs(); setupForms(); setupTranslation(); loadDashboard(); loadDashboardTokenStats(); loadTranslationStatus(); loadOrgs(); }); function logout() { localStorage.removeItem("token"); localStorage.removeItem("username"); window.location.href = "/"; } // --- Navigation --- function setupNavTabs() { document.querySelectorAll(".nav-tabs:not(#orgDetailTabs):not(#sourceSubTabs):not(#healthSubTabs) .nav-tab").forEach(tab => { tab.addEventListener("click", () => { const section = tab.dataset.section; document.querySelectorAll(".nav-tabs:not(#orgDetailTabs):not(#sourceSubTabs):not(#healthSubTabs) .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 === "dashboard") loadTranslationStatus(); if (section === "audit" && typeof loadAudit === "function") loadAudit(); }); }); } function setupOrgDetailTabs() { // Das Org-Detail ist eine durchgehende Seite, die Pillen scrollen nur noch // zum jeweiligen Bereich, statt Unterreiter umzuschalten. document.querySelectorAll("#orgDetailTabs .nav-tab").forEach(tab => { tab.addEventListener("click", () => { document.querySelectorAll("#orgDetailTabs .nav-tab").forEach(t => t.classList.remove("active")); tab.classList.add("active"); const block = document.getElementById(`sub-${tab.dataset.subtab}`); if (block) block.scrollIntoView({ behavior: "smooth", block: "start" }); }); }); 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"); // Kacheln sind Sprungziele. data-goto wechselt den Tab, data-href oeffnet // extern (Vorfaelle leben im Monitor, nicht im Portal). document.getElementById("statsGrid").innerHTML = ` `; wireStatCardLinks(); // Expiring licenses const expList = document.getElementById("expiringList"); if (stats.expiring_licenses.length === 0) { expList.innerHTML = '
  • Keine ablaufenden Lizenzen
  • '; } else { expList.innerHTML = stats.expiring_licenses.map(l => ` `).join(""); } // Recent activity const actEl = document.getElementById("recentActivity"); if (stats.recent_activity.length === 0) { actEl.innerHTML = '
    Keine Aktivität
    '; } else { actEl.innerHTML = stats.recent_activity.map(a => `
    ${a.type === "org" ? "O" : "U"}
    ${esc(a.label)}
    ${formatDate(a.created_at)}
    `).join(""); } } catch (err) { console.error("Dashboard laden fehlgeschlagen:", err); } } // --- Organizations --- let orgUsageMap = {}; // org_id -> Kontingent-Daten aus /api/token-usage/overview async function loadOrgs() { try { // Kontingent-Stand parallel holen, damit die Tabelle ihn direkt zeigt const [orgs, overview] = await Promise.all([ API.get("/api/orgs"), API.get("/api/token-usage/overview").catch(() => []), ]); orgsCache = orgs; orgUsageMap = {}; (overview || []).forEach(o => { orgUsageMap[o.org_id] = o; }); renderOrgTable(orgsCache); } catch (err) { console.error("Orgs laden fehlgeschlagen:", err); } } // Kontingent-Zelle: Verbraucht/Verfuegbar mit Mini-Balken, ∞ bei unlimitiert function orgUsageCell(orgId) { const u = orgUsageMap[orgId]; if (!u) return '-'; if (u.unlimited_budget) return ''; const avail = u.credits_available || 0; if (!avail) return '-'; const used = Math.round(u.credits_used || 0); const pct = Math.min(100, Math.round((used / avail) * 100)); const cls = pct > 80 ? "critical" : (pct > 50 ? "warning" : ""); return `
    ${used.toLocaleString('de-DE')} / ${Math.round(avail).toLocaleString('de-DE')}
    `; } function renderOrgTable(orgs) { const tbody = document.getElementById("orgTable"); if (orgs.length === 0) { tbody.innerHTML = 'Keine Organisationen'; return; } tbody.innerHTML = orgs.map(o => ` ${esc(o.name)} ${esc(o.slug)} ${o.user_count} ${o.license_type || "Keine"} ${orgUsageCell(o.id)} ${o.is_active ? "Aktiv" : "Inaktiv"} `).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"); // Erste Pille aktiv (Verbrauch), Ansicht nach oben document.querySelectorAll("#orgDetailTabs .nav-tab").forEach(t => t.classList.remove("active")); const firstPill = document.querySelector("#orgDetailTabs .nav-tab"); if (firstPill) firstPill.classList.add("active"); window.scrollTo({ top: 0 }); try { const org = await API.get(`/api/orgs/${orgId}`); document.getElementById("orgDetailHeader").innerHTML = `

    ${esc(org.name)} ${org.is_active ? "Aktiv" : "Inaktiv"}

    Slug: ${esc(org.slug)} | Nutzer: ${org.user_count} | Lizenz: ${org.license_type || "Keine"} | Kontingent: wird geladen …
    `; document.getElementById("editOrgName").value = org.name; document.getElementById("editOrgActive").value = org.is_active ? "true" : "false"; const langEl = document.getElementById("editOrgLanguage"); if (langEl) langEl.value = org.output_language || "de"; // Alle Bereiche sofort laden, es gibt keine Unterreiter mehr loadOrgUsers(orgId); loadOrgLicenses(orgId); loadOrgTokenUsage(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 = 'Keine Nutzer'; return; } tbody.innerHTML = users.map(u => ` ${esc(u.email)} ${u.is_active ? "Aktiv" : "Inaktiv"} ${u.globe_access ? '' : ''} ${u.network_access ? '' : ''} ${u.is_active ? `` : `` } `).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) { showToast(err.message, "error"); 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) { showToast(err.message, "error"); } } async function toggleGlobeAccess(userId) { try { await API.put("/api/users/" + userId + "/globe-access"); if (currentOrgId) loadOrgUsers(currentOrgId); } catch (err) { showToast(err.message, "error"); if (currentOrgId) loadOrgUsers(currentOrgId); } } async function toggleNetworkAccess(userId) { try { await API.put("/api/users/" + userId + "/network-access"); if (currentOrgId) loadOrgUsers(currentOrgId); } catch (err) { showToast(err.message, "error"); if (currentOrgId) loadOrgUsers(currentOrgId); } } function confirmDeleteUser(userId, email) { showConfirm( "Nutzer löschen", `Soll der Nutzer "${email}" endgültig gelöscht werden?`, async () => { try { await API.del(`/api/users/${userId}`); if (currentOrgId) loadOrgUsers(currentOrgId); } catch (err) { showToast(err.message, "error"); } } ); } // --- 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 = 'Keine Lizenzen'; return; } tbody.innerHTML = licenses.map(l => ` ${l.license_type} ${l.max_users} ${formatDate(l.valid_from)} ${l.valid_until ? formatDate(l.valid_until) : "Unbegrenzt"} ${l.status} ${l.status === "active" ? ` ` : ""} `).join(""); } catch (err) { console.error("Lizenzen laden fehlgeschlagen:", err); } } // Lizenz verlängern über ein eigenes Modal statt des rohen Browser-prompt() let _extendLicenseId = null; function extendLicense(licId) { _extendLicenseId = licId; const errEl = document.getElementById("extendLicenseError"); if (errEl) errEl.style.display = "none"; openModal("modalExtendLicense"); const input = document.getElementById("extendDays"); if (input) { input.value = "365"; input.focus(); } } document.addEventListener("DOMContentLoaded", function () { const form = document.getElementById("extendLicenseForm"); if (!form) return; form.addEventListener("submit", async function (e) { e.preventDefault(); const errEl = document.getElementById("extendLicenseError"); if (errEl) errEl.style.display = "none"; const days = parseInt(document.getElementById("extendDays").value); if (!days || days < 1) return; try { await API.put(`/api/licenses/${_extendLicenseId}/extend?days=${days}`); closeModal("modalExtendLicense"); if (currentOrgId) loadOrgLicenses(currentOrgId); loadDashboard(); } catch (err) { if (errEl) { errEl.textContent = err.message; errEl.style.display = "block"; } } }); }); function confirmRevokeLicense(licId) { showConfirm( "Lizenz widerrufen", "Soll die Lizenz wirklich widerrufen werden? Nutzer können dann nur noch lesen.", async () => { try { await API.put(`/api/licenses/${licId}/revoke`); if (currentOrgId) loadOrgLicenses(currentOrgId); } catch (err) { showToast(err.message, "error"); } } ); } // --- Expiring Licenses (global view) --- async function loadExpiringLicenses() { // Der globale Lizenzen-Reiter wurde entfernt, die Ablauf-Liste lebt auf dem // Dashboard. Falls die Elemente fehlen, ist hier nichts zu tun. const daysEl = document.getElementById("expiringDays"); if (!daysEl || !document.getElementById("expiringTable")) return; const days = daysEl.value; try { const licenses = await API.get(`/api/licenses/expiring?days=${days}`); const tbody = document.getElementById("expiringTable"); if (licenses.length === 0) { tbody.innerHTML = `Keine ablaufenden Lizenzen in den nächsten ${days} Tagen`; return; } tbody.innerHTML = licenses.map(l => ` ${esc(l.org_name)} ${l.license_type} ${l.max_users} ${formatDate(l.valid_until)} `).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):not(#sourceSubTabs):not(#healthSubTabs) .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"; const body = { name: document.getElementById("newOrgName").value, slug: document.getElementById("newOrgSlug").value, output_language: document.getElementById("newOrgLanguage").value || "de", }; // Optional direkt eine Lizenz mit Credits-Kontingent mitgeben const withLic = document.getElementById("newOrgWithLicense"); if (withLic && withLic.checked) { const licType = document.getElementById("newOrgLicType").value; const lic = { license_type: licType, max_users: parseInt(document.getElementById("newOrgLicMaxUsers").value) || 5, unlimited_budget: !!document.getElementById("newOrgLicUnlimited").checked, // Periode und Uebertrag IMMER senden, sonst laesst sich der // Uebertrag ueber das Formular nie abschalten credits_period: document.getElementById("newOrgLicCreditsPeriod").value, credits_rollover: !!document.getElementById("newOrgLicRollover").checked, }; if (licType !== "permanent") { lic.duration_days = parseInt(document.getElementById("newOrgLicDuration").value) || undefined; } if (!lic.unlimited_budget) { const ct = document.getElementById("newOrgLicCreditsTotal"); const cpc = document.getElementById("newOrgLicCostPerCredit"); if (ct && ct.value) lic.credits_total = parseInt(ct.value); if (cpc && cpc.value) lic.cost_per_credit = parseFloat(cpc.value); } const warn = document.getElementById("newOrgLicWarnPercent"); if (warn && warn.value) lic.budget_warning_percent = parseInt(warn.value); body.license = lic; } try { await API.post("/api/orgs", body); closeModal("modalNewOrg"); document.getElementById("newOrgForm").reset(); onNewOrgLicenseToggle(); 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, 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); } const unlimitedEl = document.getElementById('newLicUnlimited'); body.unlimited_budget = !!(unlimitedEl && unlimitedEl.checked); // Periode und Uebertrag IMMER senden (nicht nur bei gesetztem Wert), // sonst laesst sich der Uebertrag nie abschalten body.credits_period = document.getElementById('newLicCreditsPeriod').value; body.credits_rollover = !!document.getElementById('newLicRollover').checked; if (!body.unlimited_budget) { const creditsTotal = document.getElementById('newLicCreditsTotal'); const costPerCredit = document.getElementById('newLicCostPerCredit'); const budgetUsd = document.getElementById('newLicBudgetUsd'); if (creditsTotal && creditsTotal.value) body.credits_total = parseInt(creditsTotal.value); if (costPerCredit && costPerCredit.value) body.cost_per_credit = parseFloat(costPerCredit.value); if (budgetUsd && budgetUsd.value) body.token_budget_usd = parseFloat(budgetUsd.value); } const warnEl = document.getElementById('newLicWarnPercent'); if (warnEl && warnEl.value) body.budget_warning_percent = parseInt(warnEl.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", output_language: document.getElementById("editOrgLanguage").value || "de", }); openOrg(currentOrgId); loadOrgs(); loadDashboard(); } catch (err) { showToast(err.message, "error"); } }); // Delete Org document.getElementById("deleteOrgBtn").addEventListener("click", () => { showConfirm( "Organisation löschen", "Soll die Organisation mit allen Nutzern und Lizenzen endgültig gelöscht werden? Diese Aktion kann nicht rückgängig 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) { showToast(err.message, "error"); } } ); }); } // --- 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; let confirmResolver = null; function showConfirm(title, text, callback) { document.getElementById("confirmTitle").textContent = title; document.getElementById("confirmText").textContent = text; // Backward-compat: legacy Callback wird bei OK aufgerufen confirmCallback = callback || null; openModal("modalConfirm"); return new Promise((resolve) => { if (confirmResolver) confirmResolver(false); // alten Resolver schliessen confirmResolver = resolve; }); } function showToast(msg, type) { type = type || "info"; const c = document.getElementById("toastContainer"); if (!c) { console.log("[toast]", type, msg); return; } const el = document.createElement("div"); el.className = "toast toast-" + type; el.textContent = msg; c.appendChild(el); setTimeout(() => { el.classList.add("toast-out"); setTimeout(() => el.remove(), 220); }, type === "error" ? 6000 : 3500); } document.addEventListener("DOMContentLoaded", () => { document.getElementById("confirmOkBtn").addEventListener("click", async () => { closeModal("modalConfirm"); const cb = confirmCallback; const rs = confirmResolver; confirmCallback = null; confirmResolver = null; if (cb) { try { await cb(); } catch (e) { showToast(e.message || String(e), "error"); } } if (rs) rs(true); }); // Cancel/Close -> resolver(false) function _confirmCancel() { const rs = confirmResolver; confirmCallback = null; confirmResolver = null; if (rs) rs(false); } document.getElementById("confirmCancelBtn")?.addEventListener("click", _confirmCancel); document.querySelector("#modalConfirm .modal-close")?.addEventListener("click", _confirmCancel); }); // --- 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; } } // ===== Artikel-Übersetzung ===== let translationPollTimer = null; function setupTranslation() { const runBtn = document.getElementById("translationRunBtn"); const cancelBtn = document.getElementById("translationCancelBtn"); if (runBtn) runBtn.addEventListener("click", startTranslation); if (cancelBtn) cancelBtn.addEventListener("click", cancelTranslation); } function formatDuration(seconds) { seconds = Math.max(0, Math.round(seconds || 0)); if (seconds < 60) return seconds + " Sek."; const min = Math.round(seconds / 60); if (min < 60) return min + " Min."; const h = Math.floor(min / 60), m = min % 60; return h + " Std. " + (m ? m + " Min." : "").trim(); } function renderTranslation(st) { const info = document.getElementById("translationInfo"); const wrap = document.getElementById("translationProgressWrap"); const bar = document.getElementById("translationProgressBar"); const ptext = document.getElementById("translationProgressText"); const runBtn = document.getElementById("translationRunBtn"); const cancelBtn = document.getElementById("translationCancelBtn"); if (!info || !runBtn) return; if (st.running) { runBtn.style.display = "none"; cancelBtn.style.display = ""; wrap.style.display = ""; const pct = st.total > 0 ? Math.round((st.done / st.total) * 100) : 0; bar.style.width = pct + "%"; ptext.textContent = `${st.done} / ${st.total} verarbeitet, ${st.translated} übersetzt (${pct}%)`; info.textContent = "Übersetzung läuft…"; return; } runBtn.style.display = ""; cancelBtn.style.display = "none"; wrap.style.display = "none"; let resultLine = ""; if (st.finished_at && (st.total > 0 || st.error)) { if (st.error) { resultLine = `Letzter Lauf mit Fehler beendet: ${st.error}. `; } else if (st.cancelled) { resultLine = `Letzter Lauf abgebrochen, ${st.translated} von ${st.total} Artikeln übersetzt. `; } else { resultLine = `Letzter Lauf abgeschlossen, ${st.translated} Artikel übersetzt. `; } } if (st.pending > 0) { const est = st.estimate || {}; info.textContent = resultLine + `${st.pending} Artikel ohne deutsche Übersetzung. ` + `Geschätzt: ${formatDuration(est.seconds)}, ca. $${est.cost_usd}.`; runBtn.disabled = false; } else { info.textContent = resultLine + "Alle Artikel sind übersetzt."; runBtn.disabled = true; } } async function loadTranslationStatus() { try { const st = await API.get("/api/translation/status"); renderTranslation(st); if (st.running && !translationPollTimer) { translationPollTimer = setInterval(pollTranslation, 3000); } } catch (e) { const info = document.getElementById("translationInfo"); if (info) info.textContent = "Status nicht abrufbar: " + (e.message || e); } } async function pollTranslation() { try { const st = await API.get("/api/translation/status"); renderTranslation(st); if (!st.running) { clearInterval(translationPollTimer); translationPollTimer = null; if (st.error) { showToast("Übersetzung mit Fehler beendet", "error"); } else if (st.cancelled) { showToast(`Übersetzung abgebrochen, ${st.translated} übersetzt`, "info"); } else { showToast(`Übersetzung fertig: ${st.translated} Artikel`, "success"); } } } catch (e) { console.warn("Translation-Poll fehlgeschlagen:", e); } } async function startTranslation() { let st; try { st = await API.get("/api/translation/status"); } catch (e) { showToast(e.message || "Status nicht abrufbar", "error"); return; } if (st.running) { showToast("Es läuft bereits eine Übersetzung", "info"); return; } if (!st.pending) { showToast("Es gibt nichts zu übersetzen", "info"); return; } const est = st.estimate || {}; const ok = await showConfirm( "Übersetzung starten", `${st.pending} Artikel werden ins Deutsche übersetzt. ` + `Geschätzte Dauer: ${formatDuration(est.seconds)}, geschätzte Kosten: ca. $${est.cost_usd}. ` + `Der Lauf kann jederzeit abgebrochen werden.` ); if (!ok) return; try { const res = await API.post("/api/translation/run", {}); if (res && res.status === "started") { showToast(`Übersetzung gestartet (${res.pending} Artikel)`, "success"); await loadTranslationStatus(); if (!translationPollTimer) { translationPollTimer = setInterval(pollTranslation, 3000); } } else { showToast("Es gibt nichts zu übersetzen", "info"); loadTranslationStatus(); } } catch (e) { showToast(e.message || "Start fehlgeschlagen", "error"); } } async function cancelTranslation() { try { await API.post("/api/translation/cancel", {}); showToast("Übersetzung wird abgebrochen…", "info"); } catch (e) { showToast(e.message || "Abbruch fehlgeschlagen", "error"); } } // ===== Token-Nutzung ===== async function loadOrgTokenUsage(orgId) { try { const [current, monthly] = await Promise.all([ API.get('/api/token-usage/' + orgId + '/current'), API.get('/api/token-usage/' + orgId), ]); const budget = current.budget || {}; const usage = current.usage || {}; const unlimited = !!budget.unlimited_budget; const el = (id, val) => { const e = document.getElementById(id); if (e) e.textContent = val; }; el('tokenCreditsUsed', budget.credits_used != null ? Math.round(budget.credits_used).toLocaleString('de-DE') : '-'); if (unlimited) { el('tokenCreditsRemaining', '∞ Unbegrenzt'); el('tokenBudgetUsd', '—'); } else { var remainingTxt = budget.credits_remaining != null ? budget.credits_remaining.toLocaleString('de-DE') : '-'; if (budget.credits_carried > 0 && budget.credits_remaining != null) { remainingTxt += ' (inkl. ' + Math.round(budget.credits_carried).toLocaleString('de-DE') + ' Übertrag)'; } el('tokenCreditsRemaining', remainingTxt); el('tokenBudgetUsd', budget.token_budget_usd != null ? '$' + Number(budget.token_budget_usd).toFixed(2) : '-'); } el('tokenCostUsd', usage.total_cost_usd != null ? '$' + Number(usage.total_cost_usd).toFixed(2) : '-'); // Source-Split anzeigen var bySource = current.usage_by_source || {}; var splitHtml = ''; if (bySource.monitor) splitHtml += 'Monitor $' + Number(bySource.monitor.total_cost_usd).toFixed(2) + ' (' + bySource.monitor.api_calls + ' Calls) '; if (bySource.globe) splitHtml += 'Globe $' + Number(bySource.globe.total_cost_usd).toFixed(2) + ' (' + bySource.globe.api_calls + ' Calls)'; var splitEl = document.getElementById('tokenSourceSplit'); if (splitEl) splitEl.innerHTML = splitHtml; const bar = document.getElementById('tokenBudgetBar'); const percentEl = document.getElementById('tokenBudgetPercent'); const barWrap = bar ? bar.parentElement : null; if (unlimited) { if (bar) bar.style.width = '0%'; if (percentEl) percentEl.textContent = 'Unbegrenzt'; if (barWrap) barWrap.style.opacity = '0.4'; } else { const percent = budget.credits_percent_used || 0; if (bar) { bar.style.width = Math.min(100, percent) + '%'; bar.classList.remove('warning', 'critical', 'over-limit'); if (percent > 100) bar.classList.add('over-limit'); else if (percent > 80) bar.classList.add('critical'); else if (percent > 50) bar.classList.add('warning'); } if (percentEl) { percentEl.textContent = percent > 100 ? ('UEBER LIMIT (' + percent.toFixed(1) + '%)') : (percent.toFixed(1) + '%'); percentEl.style.color = percent > 100 ? 'var(--danger-text, #991b1b)' : ''; percentEl.style.fontWeight = percent > 100 ? '700' : ''; } if (barWrap) barWrap.style.opacity = '1'; } fillBudgetForm(budget); // Kontingent-Kurzinfo im Org-Kopf const kpi = document.getElementById("orgKpiCredits"); if (kpi) { if (unlimited) kpi.textContent = "unbegrenzt"; else if (budget.credits_available) { const pct = budget.credits_percent_used != null ? ` (${budget.credits_percent_used.toFixed(0)}% verbraucht)` : ""; kpi.textContent = `${Math.round(budget.credits_used || 0).toLocaleString("de-DE")} von ${Math.round(budget.credits_available).toLocaleString("de-DE")} Credits${pct}`; } else kpi.textContent = "kein Kontingent"; } const tbody = document.getElementById('tokenMonthlyTable'); if (tbody) { tbody.innerHTML = monthly.map(function(m) { var srcLabel = (m.source || 'monitor') === 'globe' ? 'Globe' : 'Monitor'; var srcClass = (m.source || 'monitor') === 'globe' ? 'badge-globe' : 'badge-monitor'; return '' + '' + esc(m.year_month) + '' + '' + srcLabel + '' + '' + m.refresh_count + '' + '' + m.api_calls.toLocaleString('de-DE') + '' + '' + m.input_tokens.toLocaleString('de-DE') + '' + '' + m.output_tokens.toLocaleString('de-DE') + '' + '$' + Number(m.total_cost_usd).toFixed(2) + '' + ''; }).join(''); } } catch (err) { console.error('Token-Usage laden fehlgeschlagen:', err); } } // Budget-Formular Felder befuellen function fillBudgetForm(budget) { const el = (id, val) => { const e = document.getElementById(id); if (e && val != null) e.value = val; }; el('editCreditsTotal', budget.credits_total); el('editCostPerCredit', budget.cost_per_credit); el('editBudgetUsd', budget.token_budget_usd); el('editCreditsUsed', budget.credits_used ? Math.round(budget.credits_used) : 0); el('editWarnPercent', budget.budget_warning_percent); const periodSel = document.getElementById('editCreditsPeriod'); if (periodSel) periodSel.value = budget.credits_period || 'monthly'; const rolloverCb = document.getElementById('editCreditsRollover'); if (rolloverCb) rolloverCb.checked = !!budget.credits_rollover; onEditPeriodChange(); const cb = document.getElementById('editUnlimitedBudget'); if (cb) { cb.checked = !!budget.unlimited_budget; onUnlimitedToggle(); } // Laufzeitwerte (verwaltet der Monitor, hier nur Anzeige) const txt = (id, val) => { const e = document.getElementById(id); if (e) e.textContent = val; }; const carried = budget.credits_carried || 0; txt('viewCreditsCarried', carried > 0 ? Math.round(carried).toLocaleString('de-DE') + ' Credits' : 'Kein Übertrag'); txt('viewCreditsPeriodStart', budget.credits_period_start || 'Noch nicht initialisiert'); txt('viewCreditsAvailable', budget.unlimited_budget ? '∞ Unbegrenzt' : (budget.credits_available != null ? Math.round(budget.credits_available).toLocaleString('de-DE') + ' Credits' : '-')); } // Unlimited-Toggle: Felder ausgrauen wenn aktiv function onUnlimitedToggle() { const cb = document.getElementById('editUnlimitedBudget'); const isUnlimited = cb && cb.checked; ['editCreditsTotal','editCostPerCredit','editBudgetUsd','editCreditsPeriod','editCreditsRollover','editWarnPercent'].forEach(function(id) { const e = document.getElementById(id); if (e) { e.disabled = isUnlimited; e.style.opacity = isUnlimited ? '0.4' : '1'; } }); if (!isUnlimited) onEditPeriodChange(); } // Bei Periode "Gesamtlaufzeit" ist der Monats-Uebertrag bedeutungslos function onEditPeriodChange() { const sel = document.getElementById('editCreditsPeriod'); const cb = document.getElementById('editCreditsRollover'); if (!sel || !cb) return; const unlimitedCb = document.getElementById('editUnlimitedBudget'); cb.disabled = sel.value === 'total' || !!(unlimitedCb && unlimitedCb.checked); cb.parentElement.style.opacity = cb.disabled ? '0.4' : '1'; } // Unlimited-Toggle im Lizenz-Modal function onNewLicUnlimitedToggle() { const cb = document.getElementById('newLicUnlimited'); const isUnlimited = cb && cb.checked; ['newLicCreditsTotal','newLicCostPerCredit','newLicBudgetUsd','newLicCreditsPeriod','newLicRollover','newLicWarnPercent'].forEach(function(id) { const e = document.getElementById(id); if (e) { e.disabled = isUnlimited; e.style.opacity = isUnlimited ? '0.4' : '1'; } }); if (!isUnlimited) onNewLicPeriodChange(); } function onNewLicPeriodChange() { const sel = document.getElementById('newLicCreditsPeriod'); const cb = document.getElementById('newLicRollover'); if (!sel || !cb) return; cb.disabled = sel.value === 'total'; cb.parentElement.style.opacity = cb.disabled ? '0.4' : '1'; } // === Lizenz-Abschnitt im Org-Modal === function onNewOrgLicenseToggle() { const cb = document.getElementById('newOrgWithLicense'); const fields = document.getElementById('newOrgLicenseFields'); if (fields) fields.style.display = (cb && cb.checked) ? '' : 'none'; } function onNewOrgLicTypeChange() { const type = document.getElementById('newOrgLicType').value; const group = document.getElementById('newOrgLicDurationGroup'); if (group) group.style.display = type === 'permanent' ? 'none' : ''; if (type !== 'permanent') { document.getElementById('newOrgLicDuration').value = type === 'trial' ? '14' : '365'; } } function onNewOrgLicUnlimitedToggle() { const cb = document.getElementById('newOrgLicUnlimited'); const isUnlimited = cb && cb.checked; ['newOrgLicCreditsTotal','newOrgLicCostPerCredit','newOrgLicCreditsPeriod','newOrgLicRollover','newOrgLicWarnPercent'].forEach(function(id) { const e = document.getElementById(id); if (e) { e.disabled = isUnlimited; e.style.opacity = isUnlimited ? '0.4' : '1'; } }); if (!isUnlimited) onNewOrgLicPeriodChange(); } function onNewOrgLicPeriodChange() { const sel = document.getElementById('newOrgLicCreditsPeriod'); const cb = document.getElementById('newOrgLicRollover'); if (!sel || !cb) return; cb.disabled = sel.value === 'total'; cb.parentElement.style.opacity = cb.disabled ? '0.4' : '1'; } async function loadDashboardTokenStats() { try { const data = await API.get('/api/token-usage/overview'); const grid = document.getElementById('statsGrid'); if (grid && data.length > 0) { const totals = data.reduce(function(acc, org) { return { cost: acc.cost + (org.total_cost_usd || 0), refreshes: acc.refreshes + (org.total_refreshes || 0), }; }, { cost: 0, refreshes: 0 }); const card = document.createElement('div'); card.className = 'stat-card'; card.innerHTML = '
    API-Kosten gesamt
    ' + '
    $' + totals.cost.toFixed(2) + '
    ' + '
    ' + totals.refreshes + ' Refreshes
    '; grid.appendChild(card); } } catch (err) { console.error('Token-Overview laden fehlgeschlagen:', err); } } // Budget-Formular Submit document.addEventListener('DOMContentLoaded', function() { var form = document.getElementById('tokenBudgetForm'); if (form) { form.addEventListener('submit', async function(e) { e.preventDefault(); var msgEl = document.getElementById('tokenBudgetMsg'); if (msgEl) msgEl.textContent = 'Speichern...'; // Lizenz-ID fuer die aktuelle Org ermitteln try { var licenses = await API.get('/api/licenses?org_id=' + currentOrgId); var activeLic = licenses.find(function(l) { return l.status === 'active'; }); if (!activeLic) { if (msgEl) msgEl.textContent = 'Keine aktive Lizenz gefunden'; return; } var body = {}; var unlimitedCb = document.getElementById('editUnlimitedBudget'); var isUnlimited = !!(unlimitedCb && unlimitedCb.checked); body.unlimited_budget = isUnlimited; var creditsTotal = document.getElementById('editCreditsTotal'); var costPerCredit = document.getElementById('editCostPerCredit'); var budgetUsd = document.getElementById('editBudgetUsd'); var creditsUsed = document.getElementById('editCreditsUsed'); if (!isUnlimited) { if (creditsTotal && creditsTotal.value) body.credits_total = parseInt(creditsTotal.value); if (costPerCredit && costPerCredit.value) body.cost_per_credit = parseFloat(costPerCredit.value); if (budgetUsd && budgetUsd.value) body.token_budget_usd = parseFloat(budgetUsd.value); // Periode und Uebertrag IMMER senden (nicht ueber das if-value-Muster), // sonst laesst sich der Uebertrag nie abschalten und die Periode nie // zurueckstellen. Laufzeitfelder (credits_carried, credits_period_start, // budget_warning_sent) sendet das Portal grundsaetzlich NICHT. body.credits_period = document.getElementById('editCreditsPeriod').value; body.credits_rollover = !!document.getElementById('editCreditsRollover').checked; var warnPercent = document.getElementById('editWarnPercent'); if (warnPercent && warnPercent.value) body.budget_warning_percent = parseInt(warnPercent.value); } // credits_used immer mitsenden (auch im Unlimited-Modus, fuer Korrekturen) if (creditsUsed && creditsUsed.value !== '') body.credits_used = parseFloat(creditsUsed.value); var result = await API.put('/api/token-usage/budget/' + activeLic.id, body); if (msgEl) { if (result && result.warning) { msgEl.textContent = result.warning; msgEl.style.color = 'var(--danger-text, #991b1b)'; } else { msgEl.textContent = 'Gespeichert!'; msgEl.style.color = ''; } } setTimeout(function() { if (msgEl) { msgEl.textContent = ''; msgEl.style.color = ''; } }, 5000); // Daten neu laden loadOrgTokenUsage(currentOrgId); } catch (err) { if (msgEl) msgEl.textContent = 'Fehler: ' + err.message; console.error('Budget speichern fehlgeschlagen:', err); } }); } }); // === Source-Meta (Kategorien + Typen aus dem Backend) === window.META = { categories: [], types: [] }; window.CATEGORY_LABELS = {}; window.TYPE_LABELS = {}; async function loadMeta() { try { const data = await API.get("/api/sources/meta"); window.META = data; window.CATEGORY_LABELS = Object.fromEntries((data.categories || []).map(c => [c.key, c.label])); window.TYPE_LABELS = Object.fromEntries((data.types || []).map(t => [t.key, t.label])); return data; } catch (err) { console.warn("loadMeta:", err); return null; } } function categoryLabel(key) { return window.CATEGORY_LABELS[key] || key || ""; } function typeLabel(key) { return window.TYPE_LABELS[key] || key || ""; } function populateSelect(el, items, allLabel) { if (!el) return; const current = el.value; el.innerHTML = ''; items.forEach(it => { const opt = document.createElement("option"); opt.value = it.key; opt.textContent = it.label; el.appendChild(opt); }); if (current && items.some(it => it.key === current)) el.value = current; } document.addEventListener("DOMContentLoaded", () => { // Beim Page-Load Meta einmalig laden (asynchron, blockiert nicht) if (window.API && (localStorage.getItem("token") || window.location.pathname === "/dashboard")) { loadMeta(); } }); // --- Verbrauchsrechner nachladen (eigenstaendiges Modul, siehe rechner.js) --- (function () { var s = document.createElement("script"); s.src = "/static/js/rechner.js?v=20260725a"; document.head.appendChild(s); })();