feat(org-detail): Einstellungen-Block aufgelöst, alles direkt einstellbar

Der frühere Bereich Einstellungen ist weg, seine Inhalte sind dorthin
gewandert, wo man sie sieht.

- Org-Status (Aktiv/Deaktiviert) und Pipeline-Sprache sind neue Spalten
  in der Lizenz-Tabelle, in der aktiven Zeile direkt umschaltbar mit
  Sofort-Speicherung und Toast, widerrufene Zeilen zeigen einen Strich.
- Umbenennen (kleines Modal) und Organisation löschen (mit Rückfrage)
  sitzen als Knöpfe in der Kopfzeile der Organisation.
- Das Org-Detail besteht damit nur noch aus zwei Bereichen, Verbrauch &
  Kontingent und Nutzer. Kein Backend-Change nötig, PUT /api/orgs/{id}
  konnte Name, Status und Sprache schon immer einzeln ändern.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
claude-dev
2026-07-25 15:42:42 +00:00
Ursprung 863e8835e4
Commit 2c3087e389
2 geänderte Dateien mit 142 neuen und 80 gelöschten Zeilen

Datei anzeigen

@@ -177,40 +177,9 @@
</div>
</div>
<!-- Bereich: Einstellungen -->
<div class="org-block" id="sub-settings">
<h2 class="org-block-title">Einstellungen</h2>
<div class="card">
<div class="card-body">
<form id="orgEditForm">
<div class="form-group">
<label for="editOrgName">Name</label>
<input type="text" id="editOrgName">
</div>
<div class="form-group">
<label>Status</label>
<select id="editOrgActive">
<option value="true">Aktiv</option>
<option value="false">Deaktiviert</option>
</select>
</div>
<div class="form-group">
<label for="editOrgLanguage">Pipeline-Sprache</label>
<select id="editOrgLanguage">
<option value="de">Deutsch</option>
<option value="en">English</option>
</select>
<small class="text-secondary">Bestimmt die Ausgabesprache der KI (Lagebild, Faktencheck, Recherche) und der sichtbarsten UI-Elemente fuer alle Nutzer dieser Organisation.</small>
</div>
<div style="display: flex; gap: 8px; margin-top: 16px;">
<button type="submit" class="btn btn-primary">Speichern</button>
<button type="button" class="btn btn-danger" id="deleteOrgBtn">Organisation löschen</button>
</div>
</form>
</div>
</div>
</div>
<!-- Der fruehere Einstellungen-Block ist aufgeloest. Status und
Pipeline-Sprache sind direkt in der Lizenz-Tabelle umschaltbar,
Umbenennen und Loeschen sitzen in der Kopfzeile. -->
<div class="org-block" id="sub-tokens">
<h2 class="org-block-title">Verbrauch &amp; Kontingent</h2>
<div class="token-overview">
@@ -261,6 +230,8 @@
<th>Gültig ab</th>
<th>Gültig bis</th>
<th>Status</th>
<th>Org-Status</th>
<th>Pipeline-Sprache</th>
<th>Aktionen</th>
</tr>
</thead>
@@ -776,6 +747,29 @@
</div>
<!-- Modal: Organisation umbenennen -->
<div class="modal-overlay" id="modalRenameOrg">
<div class="modal">
<div class="modal-header">
<h3>Organisation umbenennen</h3>
<button class="modal-close" onclick="closeModal('modalRenameOrg')">&times;</button>
</div>
<form id="renameOrgForm">
<div class="modal-body">
<div class="form-group">
<label for="renameOrgName">Name</label>
<input type="text" id="renameOrgName" required maxlength="200">
</div>
<div id="renameOrgError" class="error-msg" style="display:none"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeModal('modalRenameOrg')">Abbrechen</button>
<button type="submit" class="btn btn-primary">Speichern</button>
</div>
</form>
</div>
</div>
<!-- Modal: Lizenz verlängern (ersetzt das alte Browser-prompt) -->
<div class="modal-overlay" id="modalExtendLicense">
<div class="modal">
@@ -1169,7 +1163,7 @@
</div>
<script src="/static/js/a11y.js?v=20260725a"></script>
<script src="/static/js/app.js?v=20260725g"></script>
<script src="/static/js/app.js?v=20260725h"></script>
<script src="/static/js/sources.js?v=20260522x2"></script>
<script src="/static/js/x-scraper.js?v=20260522a"></script>
<script src="/static/js/source-health.js?v=20260509l"></script>

Datei anzeigen

@@ -306,6 +306,29 @@ document.addEventListener("DOMContentLoaded", () => {
});
// --- Open Org Detail ---
let currentOrgData = null; // zuletzt geladene Org (fuer Inline-Einstellungen)
function renderOrgHeader(org) {
const kpiAlt = document.getElementById("orgKpiCredits");
const kpiText = kpiAlt ? kpiAlt.textContent : "wird geladen …";
document.getElementById("orgDetailHeader").innerHTML = `
<div style="display:flex; align-items:flex-start; gap:10px; flex-wrap:wrap;">
<div style="flex:1; min-width:260px;">
<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"} | Kontingent: <span id="orgKpiCredits">${esc(kpiText)}</span></div>
</div>
<button class="btn btn-secondary btn-small" onclick="renameOrg()" title="Organisation umbenennen">
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg>
Umbenennen
</button>
<button class="btn btn-danger btn-small" onclick="confirmDeleteOrg()" title="Organisation endgültig löschen">
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
Löschen
</button>
</div>
`;
}
async function openOrg(orgId) {
currentOrgId = orgId;
document.getElementById("orgListView").style.display = "none";
@@ -315,15 +338,8 @@ async function openOrg(orgId) {
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"} | Kontingent: <span id="orgKpiCredits">wird geladen …</span></div>
`;
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";
currentOrgData = org;
renderOrgHeader(org);
// Alle Bereiche sofort laden, es gibt keine Unterreiter mehr
loadOrgUsers(orgId);
@@ -334,6 +350,60 @@ async function openOrg(orgId) {
}
}
// --- Inline-Einstellungen (ersetzen den frueheren Einstellungen-Block) ---
async function _saveOrgSetting(patch, meldung) {
try {
await API.put(`/api/orgs/${currentOrgId}`, patch);
currentOrgData = await API.get(`/api/orgs/${currentOrgId}`);
renderOrgHeader(currentOrgData);
loadOrgLicenses(currentOrgId);
loadOrgs();
showToast(meldung, "success");
} catch (err) {
showToast(err.message, "error");
loadOrgLicenses(currentOrgId); // Auswahl auf den echten Stand zuruecksetzen
}
}
function setOrgActive(wert) {
_saveOrgSetting({ is_active: wert === "true" },
wert === "true" ? "Organisation aktiviert" : "Organisation deaktiviert");
}
function setOrgLanguage(wert) {
_saveOrgSetting({ output_language: wert }, "Pipeline-Sprache gespeichert");
}
function renameOrg() {
const input = document.getElementById("renameOrgName");
const errEl = document.getElementById("renameOrgError");
if (errEl) errEl.style.display = "none";
if (input && currentOrgData) input.value = currentOrgData.name;
openModal("modalRenameOrg");
if (input) input.focus();
}
function confirmDeleteOrg() {
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;
currentOrgData = null;
loadOrgs();
loadDashboard();
} catch (err) {
showToast(err.message, "error");
}
}
);
}
// --- Org Users ---
async function loadOrgUsers(orgId) {
try {
@@ -436,9 +506,21 @@ async function loadOrgLicenses(orgId) {
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>';
tbody.innerHTML = '<tr><td colspan="8" class="text-muted">Keine Lizenzen</td></tr>';
return;
}
// Org-Status und Pipeline-Sprache sind Org-Einstellungen und stehen nur in
// der aktiven Zeile, direkt umschaltbar (Sofort-Speicherung).
const org = currentOrgData || {};
const orgZellen = `
<td><select style="width:auto; min-width:110px;" onchange="setOrgActive(this.value)">
<option value="true"${org.is_active ? " selected" : ""}>Aktiv</option>
<option value="false"${org.is_active ? "" : " selected"}>Deaktiviert</option>
</select></td>
<td><select style="width:auto; min-width:100px;" onchange="setOrgLanguage(this.value)" title="Ausgabesprache der KI-Pipeline für alle Nutzer dieser Organisation">
<option value="de"${(org.output_language || "de") === "de" ? " selected" : ""}>Deutsch</option>
<option value="en"${org.output_language === "en" ? " selected" : ""}>English</option>
</select></td>`;
tbody.innerHTML = licenses.map(l => `
<tr>
<td><span class="badge badge-${l.license_type}">${l.license_type}</span></td>
@@ -446,6 +528,7 @@ async function loadOrgLicenses(orgId) {
<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>
${l.status === "active" ? orgZellen : '<td class="text-muted">–</td><td class="text-muted">–</td>'}
<td>
${l.status === "active" ? `
<button class="btn btn-secondary btn-small" onclick="extendLicense(${l.id})">Verlängern</button>
@@ -683,43 +766,28 @@ function setupForms() {
}
});
// Org Edit
document.getElementById("orgEditForm").addEventListener("submit", async (e) => {
// Org umbenennen (kleines Modal, Rest der frueheren Einstellungen ist
// inline in Kopfzeile und Lizenz-Tabelle gewandert)
const renameForm = document.getElementById("renameOrgForm");
if (renameForm) {
renameForm.addEventListener("submit", async (e) => {
e.preventDefault();
const errEl = document.getElementById("renameOrgError");
if (errEl) errEl.style.display = "none";
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",
name: document.getElementById("renameOrgName").value,
});
openOrg(currentOrgId);
closeModal("modalRenameOrg");
currentOrgData = await API.get(`/api/orgs/${currentOrgId}`);
renderOrgHeader(currentOrgData);
loadOrgs();
loadDashboard();
} catch (err) {
showToast(err.message, "error");
if (errEl) { errEl.textContent = err.message; errEl.style.display = "block"; }
}
});
// 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) {