/* 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(); loadDashboardTokenStats(); loadOrgs(); }); function logout() { localStorage.removeItem("token"); localStorage.removeItem("username"); window.location.href = "/"; } // --- Navigation --- function setupNavTabs() { document.querySelectorAll(".nav-tabs:not(#orgDetailTabs):not(#sourceSubTabs) .nav-tab").forEach(tab => { tab.addEventListener("click", () => { const section = tab.dataset.section; document.querySelectorAll(".nav-tabs:not(#orgDetailTabs):not(#sourceSubTabs) .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"); if (subtab === 'tokens' && currentOrgId) loadOrgTokenUsage(currentOrgId); }); }); 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 = `
Organisationen
${stats.organizations.total}
${stats.organizations.active} aktiv
Nutzer
${stats.users.total}
${stats.users.active} aktiv
Aktive Lizenzen
${stats.licenses.active}
Vorfälle
${stats.incidents.total}
${stats.incidents.active} aktiv
`; // 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 => `
  • ${esc(l.org_name)} ${l.license_type} ${formatDate(l.valid_until)}
  • `).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 --- 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 = 'Keine Organisationen'; return; } tbody.innerHTML = orgs.map(o => ` ${esc(o.name)} ${esc(o.slug)} ${o.user_count} ${o.license_type || "Keine"} ${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"); // 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 = `

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

    Slug: ${esc(org.slug)} | Nutzer: ${org.user_count} | Lizenz: ${org.license_type || "Keine"}
    `; 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 = 'Keine Nutzer'; return; } tbody.innerHTML = users.map(u => ` ${esc(u.email)} ${u.is_active ? "Aktiv" : "Inaktiv"} ${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) { 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); } } async function toggleGlobeAccess(userId) { try { await API.put("/api/users/" + userId + "/globe-access"); } catch (err) { alert(err.message); 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) { 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 = '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); } } async function extendLicense(licId) { const days = prompt("Um wie viele Tage verlängern?", "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 können 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 = `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) .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, 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 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); 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 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) { 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; } } // ===== 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 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') : '-'); el('tokenCreditsRemaining', budget.credits_remaining != null ? budget.credits_remaining.toLocaleString('de-DE') : '-'); 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) : '-'); const bar = document.getElementById('tokenBudgetBar'); const percentEl = document.getElementById('tokenBudgetPercent'); const percent = budget.credits_percent_used || 0; if (bar) { bar.style.width = Math.min(100, percent) + '%'; bar.classList.remove('warning', 'critical'); if (percent > 80) bar.classList.add('critical'); else if (percent > 50) bar.classList.add('warning'); } if (percentEl) percentEl.textContent = percent.toFixed(1) + '%'; fillBudgetForm(budget); const tbody = document.getElementById('tokenMonthlyTable'); if (tbody) { tbody.innerHTML = monthly.map(function(m) { return '' + '' + esc(m.year_month) + '' + '' + 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); } 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 creditsTotal = document.getElementById('editCreditsTotal'); var costPerCredit = document.getElementById('editCostPerCredit'); var budgetUsd = document.getElementById('editBudgetUsd'); var creditsUsed = document.getElementById('editCreditsUsed'); 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); if (creditsUsed && creditsUsed.value !== '') body.credits_used = parseFloat(creditsUsed.value); await API.put('/api/token-usage/budget/' + activeLic.id, body); if (msgEl) msgEl.textContent = 'Gespeichert!'; setTimeout(function() { if (msgEl) msgEl.textContent = ''; }, 3000); // Daten neu laden loadOrgTokenUsage(currentOrgId); } catch (err) { if (msgEl) msgEl.textContent = 'Fehler: ' + err.message; console.error('Budget speichern fehlgeschlagen:', err); } }); } });