feat(ui): alle Filter als Checkbox-Gruppen statt Dropdowns + Aufgaben-Filter
- Neuer wiederverwendbarer Helper renderFilterGroup (app.js) + filter-bar-CSS. Mehrfachauswahl per Checkbox, nichts angehakt = alle, Gold-Akzent. - Quellenliste: Herkunft/Typ/Status/Kategorie/Sprache als Checkbox-Leiste unter der Action-Bar. Checkbox-Spalte + Bulk-Promote erscheinen, wenn der Herkunfts-Filter ausschliesslich auf Kundenquellen steht. - Aufgaben ist jetzt filterbar: Vorschlagstyp und Prioritaet fuer die Vorschlags-Tabelle, Konfidenz-Stufen (ab 85, 70 bis 85, 50 bis 70, unter 50 Prozent) fuer die Klassifikations-Karten. Das alte Mindest-Konfidenz-Dropdown entfaellt, die Queue laedt einmal komplett und filtert clientseitig. - Audit-Log: Aktion/Ressource/Admin als Checkbox-Gruppen, Backend akzeptiert dafuer kommagetrennte Mehrfachwerte (IN-Filter, Einzelwert bleibt kompatibel). Ressourcen-ID und Datumsfelder bleiben Eingabefelder. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -2,7 +2,7 @@
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from auth import get_current_admin
|
from auth import get_current_admin
|
||||||
from database import db_dependency
|
from database import db_dependency
|
||||||
import aiosqlite
|
import aiosqlite
|
||||||
@@ -25,7 +25,7 @@ async def list_audit(
|
|||||||
action: Optional[str] = None,
|
action: Optional[str] = None,
|
||||||
resource_type: Optional[str] = None,
|
resource_type: Optional[str] = None,
|
||||||
resource_id: Optional[int] = None,
|
resource_id: Optional[int] = None,
|
||||||
admin_id: Optional[int] = None,
|
admin_id: Optional[str] = None,
|
||||||
from_ts: Optional[str] = None,
|
from_ts: Optional[str] = None,
|
||||||
to_ts: Optional[str] = None,
|
to_ts: Optional[str] = None,
|
||||||
limit: int = 200,
|
limit: int = 200,
|
||||||
@@ -33,7 +33,11 @@ async def list_audit(
|
|||||||
admin: dict = Depends(get_current_admin),
|
admin: dict = Depends(get_current_admin),
|
||||||
db: aiosqlite.Connection = Depends(db_dependency),
|
db: aiosqlite.Connection = Depends(db_dependency),
|
||||||
):
|
):
|
||||||
"""Audit-Log-Eintraege auflisten mit Filter + Pagination."""
|
"""Audit-Log-Eintraege auflisten mit Filter + Pagination.
|
||||||
|
|
||||||
|
action, resource_type und admin_id akzeptieren kommagetrennte Mehrfachwerte
|
||||||
|
(Checkbox-Filter im Frontend), ein Einzelwert funktioniert weiterhin.
|
||||||
|
"""
|
||||||
if limit < 1 or limit > 1000:
|
if limit < 1 or limit > 1000:
|
||||||
limit = 200
|
limit = 200
|
||||||
if offset < 0:
|
if offset < 0:
|
||||||
@@ -42,17 +46,26 @@ async def list_audit(
|
|||||||
where = []
|
where = []
|
||||||
params = []
|
params = []
|
||||||
if action:
|
if action:
|
||||||
where.append("action = ?")
|
vals = [v.strip() for v in action.split(",") if v.strip()]
|
||||||
params.append(action)
|
if vals:
|
||||||
|
where.append(f"action IN ({','.join('?' for _ in vals)})")
|
||||||
|
params.extend(vals)
|
||||||
if resource_type:
|
if resource_type:
|
||||||
where.append("resource_type = ?")
|
vals = [v.strip() for v in resource_type.split(",") if v.strip()]
|
||||||
params.append(resource_type)
|
if vals:
|
||||||
|
where.append(f"resource_type IN ({','.join('?' for _ in vals)})")
|
||||||
|
params.extend(vals)
|
||||||
if resource_id is not None:
|
if resource_id is not None:
|
||||||
where.append("resource_id = ?")
|
where.append("resource_id = ?")
|
||||||
params.append(resource_id)
|
params.append(resource_id)
|
||||||
if admin_id is not None:
|
if admin_id:
|
||||||
where.append("admin_id = ?")
|
try:
|
||||||
params.append(admin_id)
|
ids = [int(v) for v in admin_id.split(",") if v.strip()]
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPException(status_code=422, detail="admin_id muss eine Zahl oder kommagetrennte Zahlen sein")
|
||||||
|
if ids:
|
||||||
|
where.append(f"admin_id IN ({','.join('?' for _ in ids)})")
|
||||||
|
params.extend(ids)
|
||||||
if from_ts:
|
if from_ts:
|
||||||
where.append("ts >= ?")
|
where.append("ts >= ?")
|
||||||
params.append(from_ts)
|
params.append(from_ts)
|
||||||
|
|||||||
@@ -928,6 +928,49 @@ input[type="date"].filter-select { padding: 6px 10px; }
|
|||||||
.health-run-item { display: inline-flex; align-items: center; gap: 8px; }
|
.health-run-item { display: inline-flex; align-items: center; gap: 8px; }
|
||||||
.health-run-item #healthRunInfo { font-size: 12px; }
|
.health-run-item #healthRunInfo { font-size: 12px; }
|
||||||
|
|
||||||
|
/* Checkbox-Filterleisten (Standard fuer alle Filter im Portal) */
|
||||||
|
.filter-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.filter-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 4px 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.filter-group-label {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.4px;
|
||||||
|
min-width: 92px;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.filter-check {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 7px;
|
||||||
|
border-radius: 6px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.filter-check:hover { background: var(--bg-tertiary); }
|
||||||
|
.filter-check input {
|
||||||
|
accent-color: var(--accent);
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* Checkbox-Spalte: nur sichtbar bei Herkunfts-Filter "Kundenquellen" */
|
/* Checkbox-Spalte: nur sichtbar bei Herkunfts-Filter "Kundenquellen" */
|
||||||
.select-col { display: none; }
|
.select-col { display: none; }
|
||||||
table.show-select .select-col { display: table-cell; }
|
table.show-select .select-col { display: table-cell; }
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<title>AegisSight Monitor-Verwaltung</title>
|
<title>AegisSight Monitor-Verwaltung</title>
|
||||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
||||||
<link rel="apple-touch-icon" href="/static/favicon.svg">
|
<link rel="apple-touch-icon" href="/static/favicon.svg">
|
||||||
<link rel="stylesheet" href="/static/css/style.css?v=20260725j">
|
<link rel="stylesheet" href="/static/css/style.css?v=20260725k">
|
||||||
<script>(function(){var t=localStorage.getItem('portal_theme');if(t==='light')document.documentElement.setAttribute('data-theme','light');try{var a=JSON.parse(localStorage.getItem('osint_a11y')||'{}');Object.keys(a).forEach(function(k){if(a[k])document.documentElement.setAttribute('data-a11y-'+k,'true');});}catch(e){}})()</script>
|
<script>(function(){var t=localStorage.getItem('portal_theme');if(t==='light')document.documentElement.setAttribute('data-theme','light');try{var a=JSON.parse(localStorage.getItem('osint_a11y')||'{}');Object.keys(a).forEach(function(k){if(a[k])document.documentElement.setAttribute('data-a11y-'+k,'true');});}catch(e){}})()</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -296,25 +296,6 @@
|
|||||||
<div class="action-bar">
|
<div class="action-bar">
|
||||||
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
|
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
|
||||||
<input type="text" class="search-input" id="globalSourceSearch" placeholder="Quelle suchen...">
|
<input type="text" class="search-input" id="globalSourceSearch" placeholder="Quelle suchen...">
|
||||||
<select class="filter-select" id="filterOrigin" onchange="filterUnifiedSources()">
|
|
||||||
<option value="">Alle Quellen</option>
|
|
||||||
<option value="global">Nur Grundquellen</option>
|
|
||||||
<option value="tenant">Nur Kundenquellen</option>
|
|
||||||
</select>
|
|
||||||
<select class="filter-select" id="globalFilterType" onchange="filterUnifiedSources()">
|
|
||||||
<option value="">Alle Typen</option>
|
|
||||||
</select>
|
|
||||||
<select class="filter-select" id="globalFilterCategory" onchange="filterUnifiedSources()">
|
|
||||||
<option value="">Alle Kategorien</option>
|
|
||||||
</select>
|
|
||||||
<select class="filter-select" id="globalFilterStatus" onchange="filterUnifiedSources()">
|
|
||||||
<option value="">Alle Status</option>
|
|
||||||
<option value="active">Aktiv</option>
|
|
||||||
<option value="inactive">Inaktiv</option>
|
|
||||||
</select>
|
|
||||||
<select class="filter-select" id="globalFilterLanguage" onchange="filterUnifiedSources()">
|
|
||||||
<option value="">Alle Sprachen</option>
|
|
||||||
</select>
|
|
||||||
<span class="text-secondary" id="globalSourceCount"></span>
|
<span class="text-secondary" id="globalSourceCount"></span>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" id="tenantBulkPromoteBtn" onclick="bulkPromoteSelected()" disabled style="display:none;margin-right:8px;">Ausgewählte übernehmen (0)</button>
|
<button class="btn btn-primary" id="tenantBulkPromoteBtn" onclick="bulkPromoteSelected()" disabled style="display:none;margin-right:8px;">Ausgewählte übernehmen (0)</button>
|
||||||
@@ -322,6 +303,7 @@
|
|||||||
<button class="btn btn-secondary" id="newPdfSourceBtn" style="margin-right:8px;">+ PDF hochladen</button>
|
<button class="btn btn-secondary" id="newPdfSourceBtn" style="margin-right:8px;">+ PDF hochladen</button>
|
||||||
<button class="btn btn-primary" id="newGlobalSourceBtn">+ Neue Grundquelle</button>
|
<button class="btn btn-primary" id="newGlobalSourceBtn">+ Neue Grundquelle</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="filter-bar" id="unifiedFilterBar"></div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table id="unifiedTable">
|
<table id="unifiedTable">
|
||||||
@@ -355,16 +337,6 @@
|
|||||||
<div class="review-toolbar-info">
|
<div class="review-toolbar-info">
|
||||||
<span><strong id="aufgabenSuggestionCount">0</strong> Vorschläge offen</span>
|
<span><strong id="aufgabenSuggestionCount">0</strong> Vorschläge offen</span>
|
||||||
<span><strong id="reviewPendingCount">0</strong> Klassifikationen ausstehend</span>
|
<span><strong id="reviewPendingCount">0</strong> Klassifikationen ausstehend</span>
|
||||||
<label class="review-conf-filter">
|
|
||||||
Mindest-Konfidenz
|
|
||||||
<select class="filter-select" id="reviewMinConfidence" onchange="loadClassificationQueue()">
|
|
||||||
<option value="0">alle</option>
|
|
||||||
<option value="0.5">0.5+</option>
|
|
||||||
<option value="0.7">0.7+</option>
|
|
||||||
<option value="0.85">0.85+</option>
|
|
||||||
<option value="0.9">0.9+</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="review-toolbar-actions">
|
<div class="review-toolbar-actions">
|
||||||
<button class="btn btn-secondary btn-small" onclick="triggerExternalReputationSync()" title="IFCN-Faktenchecker und EUvsDisinfo neu syncen">Externe Daten syncen</button>
|
<button class="btn btn-secondary btn-small" onclick="triggerExternalReputationSync()" title="IFCN-Faktenchecker und EUvsDisinfo neu syncen">Externe Daten syncen</button>
|
||||||
@@ -372,6 +344,7 @@
|
|||||||
<button class="btn btn-primary btn-small" onclick="bulkApproveHighConfidence()" title="Alle Vorschläge ab 0.85 Konfidenz übernehmen">Alle ≥ 0.85 genehmigen</button>
|
<button class="btn btn-primary btn-small" onclick="bulkApproveHighConfidence()" title="Alle Vorschläge ab 0.85 Konfidenz übernehmen">Alle ≥ 0.85 genehmigen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="filter-bar" id="aufgabenFilterBar"></div>
|
||||||
<div id="aufgabenSuggestions"></div>
|
<div id="aufgabenSuggestions"></div>
|
||||||
<div class="card" style="margin-bottom:16px;">
|
<div class="card" style="margin-bottom:16px;">
|
||||||
<div class="card-header"><h2>Klassifikations-Review</h2></div>
|
<div class="card-header"><h2>Klassifikations-Review</h2></div>
|
||||||
@@ -419,16 +392,7 @@
|
|||||||
<div class="section" id="sec-audit">
|
<div class="section" id="sec-audit">
|
||||||
<div class="action-bar" style="flex-wrap:wrap;gap:8px;">
|
<div class="action-bar" style="flex-wrap:wrap;gap:8px;">
|
||||||
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
|
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
|
||||||
<select class="filter-select" id="auditFilterAction">
|
|
||||||
<option value="">Alle Aktionen</option>
|
|
||||||
</select>
|
|
||||||
<select class="filter-select" id="auditFilterResource">
|
|
||||||
<option value="">Alle Ressourcen</option>
|
|
||||||
</select>
|
|
||||||
<input type="number" class="filter-select" id="auditFilterResourceId" placeholder="Ressourcen-ID" min="1" style="width:130px;">
|
<input type="number" class="filter-select" id="auditFilterResourceId" placeholder="Ressourcen-ID" min="1" style="width:130px;">
|
||||||
<select class="filter-select" id="auditFilterAdmin">
|
|
||||||
<option value="">Alle Admins</option>
|
|
||||||
</select>
|
|
||||||
<input type="date" class="filter-select" id="auditFilterFrom" title="Von (Datum)">
|
<input type="date" class="filter-select" id="auditFilterFrom" title="Von (Datum)">
|
||||||
<input type="date" class="filter-select" id="auditFilterTo" title="Bis (Datum)">
|
<input type="date" class="filter-select" id="auditFilterTo" title="Bis (Datum)">
|
||||||
<button class="btn btn-secondary btn-small" id="auditFilterReset">Filter zuruecksetzen</button>
|
<button class="btn btn-secondary btn-small" id="auditFilterReset">Filter zuruecksetzen</button>
|
||||||
@@ -439,6 +403,7 @@
|
|||||||
<button class="btn btn-secondary btn-small" id="auditNextBtn" disabled>Weiter →</button>
|
<button class="btn btn-secondary btn-small" id="auditNextBtn" disabled>Weiter →</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="filter-bar" id="auditFilterBar"></div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table>
|
<table>
|
||||||
@@ -1005,11 +970,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/a11y.js?v=20260725a"></script>
|
<script src="/static/js/a11y.js?v=20260725a"></script>
|
||||||
<script src="/static/js/app.js?v=20260725s"></script>
|
<script src="/static/js/app.js?v=20260725t"></script>
|
||||||
<script src="/static/js/sources.js?v=20260725d"></script>
|
<script src="/static/js/sources.js?v=20260725e"></script>
|
||||||
<script src="/static/js/aufgaben.js?v=20260725a"></script>
|
<script src="/static/js/aufgaben.js?v=20260725b"></script>
|
||||||
<script src="/static/js/x-scraper.js?v=20260522a"></script>
|
<script src="/static/js/x-scraper.js?v=20260522a"></script>
|
||||||
<script src="/static/js/audit.js?v=20260509d"></script>
|
<script src="/static/js/audit.js?v=20260725a"></script>
|
||||||
<div id="toastContainer" class="toast-container" aria-live="polite" aria-atomic="true"></div>
|
<div id="toastContainer" class="toast-container" aria-live="polite" aria-atomic="true"></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1375,6 +1375,36 @@ function populateSelect(el, items, allLabel) {
|
|||||||
if (current && items.some(it => it.key === current)) el.value = current;
|
if (current && items.some(it => it.key === current)) el.value = current;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Checkbox-Filtergruppe (Standard für alle Filter im Portal, statt Dropdowns).
|
||||||
|
// selected ist ein Set mit den angehakten Keys. Nichts angehakt = keine
|
||||||
|
// Einschränkung. onChange wird nach jedem Toggle aufgerufen.
|
||||||
|
function renderFilterGroup(container, title, items, selected, onChange) {
|
||||||
|
if (!container) return;
|
||||||
|
const wrap = document.createElement("div");
|
||||||
|
wrap.className = "filter-group";
|
||||||
|
const lbl = document.createElement("span");
|
||||||
|
lbl.className = "filter-group-label";
|
||||||
|
lbl.textContent = title;
|
||||||
|
lbl.title = "Nichts angehakt = alle";
|
||||||
|
wrap.appendChild(lbl);
|
||||||
|
(items || []).forEach((it) => {
|
||||||
|
const label = document.createElement("label");
|
||||||
|
label.className = "filter-check";
|
||||||
|
const cb = document.createElement("input");
|
||||||
|
cb.type = "checkbox";
|
||||||
|
cb.checked = selected.has(it.key);
|
||||||
|
cb.addEventListener("change", () => {
|
||||||
|
if (cb.checked) selected.add(it.key);
|
||||||
|
else selected.delete(it.key);
|
||||||
|
onChange();
|
||||||
|
});
|
||||||
|
label.appendChild(cb);
|
||||||
|
label.appendChild(document.createTextNode(it.label));
|
||||||
|
wrap.appendChild(label);
|
||||||
|
});
|
||||||
|
container.appendChild(wrap);
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
// Beim Page-Load Meta einmalig laden (asynchron, blockiert nicht)
|
// Beim Page-Load Meta einmalig laden (asynchron, blockiert nicht)
|
||||||
if (window.API && (localStorage.getItem("token") || window.location.pathname === "/dashboard")) {
|
if (window.API && (localStorage.getItem("token") || window.location.pathname === "/dashboard")) {
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ let auditCache = { items: [], total: 0, offset: 0, limit: 200 };
|
|||||||
let auditDistinct = { actions: [], resource_types: [], admins: [] };
|
let auditDistinct = { actions: [], resource_types: [], admins: [] };
|
||||||
let expandedRows = new Set();
|
let expandedRows = new Set();
|
||||||
|
|
||||||
|
// Checkbox-Filter (Mehrfachauswahl, leeres Set = keine Einschränkung)
|
||||||
|
const auditFilters = { actions: new Set(), resources: new Set(), admins: new Set() };
|
||||||
|
|
||||||
const ACTION_LABELS = {
|
const ACTION_LABELS = {
|
||||||
create: "Erstellt",
|
create: "Erstellt",
|
||||||
update: "Geändert",
|
update: "Geändert",
|
||||||
@@ -37,20 +40,22 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
tab.addEventListener("click", () => loadAudit());
|
tab.addEventListener("click", () => loadAudit());
|
||||||
});
|
});
|
||||||
|
|
||||||
// Filter-Inputs verdrahten
|
// Filter-Inputs verdrahten (Checkbox-Gruppen laufen über renderAuditFilterBar)
|
||||||
["auditFilterAction", "auditFilterResource", "auditFilterResourceId", "auditFilterAdmin",
|
["auditFilterResourceId", "auditFilterFrom", "auditFilterTo"].forEach((id) => {
|
||||||
"auditFilterFrom", "auditFilterTo"].forEach((id) => {
|
|
||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
if (el) el.addEventListener("change", () => { auditCache.offset = 0; loadAudit(); });
|
if (el) el.addEventListener("change", () => { auditCache.offset = 0; loadAudit(); });
|
||||||
});
|
});
|
||||||
|
|
||||||
const reset = document.getElementById("auditFilterReset");
|
const reset = document.getElementById("auditFilterReset");
|
||||||
if (reset) reset.addEventListener("click", () => {
|
if (reset) reset.addEventListener("click", () => {
|
||||||
["auditFilterAction", "auditFilterResource", "auditFilterAdmin",
|
auditFilters.actions.clear();
|
||||||
"auditFilterFrom", "auditFilterTo"].forEach((id) => {
|
auditFilters.resources.clear();
|
||||||
|
auditFilters.admins.clear();
|
||||||
|
["auditFilterResourceId", "auditFilterFrom", "auditFilterTo"].forEach((id) => {
|
||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
if (el) el.value = "";
|
if (el) el.value = "";
|
||||||
});
|
});
|
||||||
|
renderAuditFilterBar();
|
||||||
auditCache.offset = 0;
|
auditCache.offset = 0;
|
||||||
loadAudit();
|
loadAudit();
|
||||||
});
|
});
|
||||||
@@ -70,57 +75,49 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function loadAuditDistinct() {
|
async function loadAuditDistinct() {
|
||||||
|
// Distinct-Werte nur einmal laden, die Leiste nur bei Bedarf neu aufbauen
|
||||||
|
// (sonst wuerde jeder Checkbox-Klick die Leiste flackern lassen).
|
||||||
|
if (auditDistinct.actions && auditDistinct.actions.length) {
|
||||||
|
const bar = document.getElementById("auditFilterBar");
|
||||||
|
if (bar && !bar.children.length) renderAuditFilterBar();
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
auditDistinct = await API.get("/api/audit-log/distinct");
|
auditDistinct = await API.get("/api/audit-log/distinct");
|
||||||
|
renderAuditFilterBar();
|
||||||
const actSel = document.getElementById("auditFilterAction");
|
|
||||||
if (actSel && actSel.options.length <= 1) {
|
|
||||||
auditDistinct.actions.forEach((a) => {
|
|
||||||
const opt = document.createElement("option");
|
|
||||||
opt.value = a;
|
|
||||||
opt.textContent = ACTION_LABELS[a] || a;
|
|
||||||
actSel.appendChild(opt);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const resSel = document.getElementById("auditFilterResource");
|
|
||||||
if (resSel && resSel.options.length <= 1) {
|
|
||||||
auditDistinct.resource_types.forEach((r) => {
|
|
||||||
const opt = document.createElement("option");
|
|
||||||
opt.value = r;
|
|
||||||
opt.textContent = RESOURCE_LABELS[r] || r;
|
|
||||||
resSel.appendChild(opt);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const admSel = document.getElementById("auditFilterAdmin");
|
|
||||||
if (admSel && admSel.options.length <= 1) {
|
|
||||||
auditDistinct.admins.forEach((a) => {
|
|
||||||
const opt = document.createElement("option");
|
|
||||||
opt.value = a.id;
|
|
||||||
opt.textContent = a.username;
|
|
||||||
admSel.appendChild(opt);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Audit-Filter laden fehlgeschlagen:", err);
|
console.error("Audit-Filter laden fehlgeschlagen:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Checkbox-Filterleiste (Aktion, Ressource, Admin) aus den Distinct-Werten
|
||||||
|
function renderAuditFilterBar() {
|
||||||
|
const bar = document.getElementById("auditFilterBar");
|
||||||
|
if (!bar) return;
|
||||||
|
bar.innerHTML = "";
|
||||||
|
const onChange = () => { auditCache.offset = 0; loadAudit(); };
|
||||||
|
renderFilterGroup(bar, "Aktion",
|
||||||
|
(auditDistinct.actions || []).map((a) => ({ key: a, label: ACTION_LABELS[a] || a })),
|
||||||
|
auditFilters.actions, onChange);
|
||||||
|
renderFilterGroup(bar, "Ressource",
|
||||||
|
(auditDistinct.resource_types || []).map((r) => ({ key: r, label: RESOURCE_LABELS[r] || r })),
|
||||||
|
auditFilters.resources, onChange);
|
||||||
|
renderFilterGroup(bar, "Admin",
|
||||||
|
(auditDistinct.admins || []).map((a) => ({ key: String(a.id), label: a.username })),
|
||||||
|
auditFilters.admins, onChange);
|
||||||
|
}
|
||||||
|
|
||||||
async function loadAudit() {
|
async function loadAudit() {
|
||||||
await loadAuditDistinct();
|
await loadAuditDistinct();
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
const action = document.getElementById("auditFilterAction")?.value;
|
|
||||||
const resource = document.getElementById("auditFilterResource")?.value;
|
|
||||||
const resourceId = document.getElementById("auditFilterResourceId")?.value;
|
const resourceId = document.getElementById("auditFilterResourceId")?.value;
|
||||||
const adminId = document.getElementById("auditFilterAdmin")?.value;
|
|
||||||
const from = document.getElementById("auditFilterFrom")?.value;
|
const from = document.getElementById("auditFilterFrom")?.value;
|
||||||
const to = document.getElementById("auditFilterTo")?.value;
|
const to = document.getElementById("auditFilterTo")?.value;
|
||||||
|
|
||||||
if (action) params.append("action", action);
|
if (auditFilters.actions.size) params.append("action", [...auditFilters.actions].join(","));
|
||||||
if (resource) params.append("resource_type", resource);
|
if (auditFilters.resources.size) params.append("resource_type", [...auditFilters.resources].join(","));
|
||||||
|
if (auditFilters.admins.size) params.append("admin_id", [...auditFilters.admins].join(","));
|
||||||
if (resourceId) params.append("resource_id", resourceId);
|
if (resourceId) params.append("resource_id", resourceId);
|
||||||
if (adminId) params.append("admin_id", adminId);
|
|
||||||
if (from) params.append("from_ts", from);
|
if (from) params.append("from_ts", from);
|
||||||
if (to) params.append("to_ts", to);
|
if (to) params.append("to_ts", to);
|
||||||
params.append("limit", auditCache.limit);
|
params.append("limit", auditCache.limit);
|
||||||
|
|||||||
@@ -3,6 +3,18 @@
|
|||||||
|
|
||||||
let suggestionsCache = [];
|
let suggestionsCache = [];
|
||||||
let healthHistoryCache = [];
|
let healthHistoryCache = [];
|
||||||
|
let classificationQueueCache = [];
|
||||||
|
|
||||||
|
// Checkbox-Filter des Aufgaben-Reiters. Leeres Set = keine Einschränkung.
|
||||||
|
const aufgabenFilters = { types: new Set(), prio: new Set(), conf: new Set() };
|
||||||
|
|
||||||
|
// Konfidenz-Stufen für den Klassifikations-Filter (Mehrfachauswahl)
|
||||||
|
const CONF_BUCKETS = [
|
||||||
|
{ key: "b85", label: "ab 85 %", test: (c) => c >= 0.85 },
|
||||||
|
{ key: "b70", label: "70 bis 85 %", test: (c) => c >= 0.7 && c < 0.85 },
|
||||||
|
{ key: "b50", label: "50 bis 70 %", test: (c) => c >= 0.5 && c < 0.7 },
|
||||||
|
{ key: "b0", label: "unter 50 %", test: (c) => c < 0.5 },
|
||||||
|
];
|
||||||
|
|
||||||
const SUGGESTION_TYPE_LABELS = {
|
const SUGGESTION_TYPE_LABELS = {
|
||||||
add_source: "Neue Quelle",
|
add_source: "Neue Quelle",
|
||||||
@@ -36,6 +48,7 @@ async function refreshTasksBadge() {
|
|||||||
|
|
||||||
// --- Aufgaben laden ---
|
// --- Aufgaben laden ---
|
||||||
async function loadAufgaben() {
|
async function loadAufgaben() {
|
||||||
|
renderAufgabenFilters();
|
||||||
try {
|
try {
|
||||||
const [suggestions, history] = await Promise.all([
|
const [suggestions, history] = await Promise.all([
|
||||||
API.get("/api/sources/suggestions"),
|
API.get("/api/sources/suggestions"),
|
||||||
@@ -52,14 +65,36 @@ async function loadAufgaben() {
|
|||||||
refreshTasksBadge();
|
refreshTasksBadge();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Checkbox-Filterleiste des Aufgaben-Reiters
|
||||||
|
function renderAufgabenFilters() {
|
||||||
|
const bar = document.getElementById("aufgabenFilterBar");
|
||||||
|
if (!bar) return;
|
||||||
|
bar.innerHTML = "";
|
||||||
|
renderFilterGroup(bar, "Vorschlagstyp",
|
||||||
|
Object.entries(SUGGESTION_TYPE_LABELS).map(([key, label]) => ({ key, label })),
|
||||||
|
aufgabenFilters.types, renderAufgabenSuggestions);
|
||||||
|
renderFilterGroup(bar, "Priorität",
|
||||||
|
Object.entries(PRIORITY_LABELS).map(([key, label]) => ({ key, label })),
|
||||||
|
aufgabenFilters.prio, renderAufgabenSuggestions);
|
||||||
|
renderFilterGroup(bar, "Konfidenz", CONF_BUCKETS.map(b => ({ key: b.key, label: b.label })),
|
||||||
|
aufgabenFilters.conf, renderClassificationList);
|
||||||
|
}
|
||||||
|
|
||||||
function renderAufgabenSuggestions() {
|
function renderAufgabenSuggestions() {
|
||||||
const pane = document.getElementById("aufgabenSuggestions");
|
const pane = document.getElementById("aufgabenSuggestions");
|
||||||
if (!pane) return;
|
if (!pane) return;
|
||||||
const pending = suggestionsCache.filter((s) => s.status === "pending");
|
const allPending = suggestionsCache.filter((s) => s.status === "pending");
|
||||||
const countEl = document.getElementById("aufgabenSuggestionCount");
|
const countEl = document.getElementById("aufgabenSuggestionCount");
|
||||||
if (countEl) countEl.textContent = String(pending.length);
|
if (countEl) countEl.textContent = String(allPending.length);
|
||||||
|
|
||||||
if (pending.length === 0) {
|
const f = aufgabenFilters;
|
||||||
|
const pending = allPending.filter((s) => {
|
||||||
|
if (f.types.size && !f.types.has(s.suggestion_type)) return false;
|
||||||
|
if (f.prio.size && !f.prio.has(s.priority)) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (allPending.length === 0) {
|
||||||
pane.innerHTML = `
|
pane.innerHTML = `
|
||||||
<div class="card" style="margin-bottom:16px;">
|
<div class="card" style="margin-bottom:16px;">
|
||||||
<div class="card-header"><h2>Vorschläge</h2></div>
|
<div class="card-header"><h2>Vorschläge</h2></div>
|
||||||
@@ -67,10 +102,21 @@ function renderAufgabenSuggestions() {
|
|||||||
</div>`;
|
</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (pending.length === 0) {
|
||||||
|
pane.innerHTML = `
|
||||||
|
<div class="card" style="margin-bottom:16px;">
|
||||||
|
<div class="card-header"><h2>Vorschläge (0 von ${allPending.length})</h2></div>
|
||||||
|
<div class="card-body text-muted">Keine Vorschläge mit diesen Filtern.</div>
|
||||||
|
</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const headCount = pending.length === allPending.length
|
||||||
|
? `${allPending.length} offen`
|
||||||
|
: `${pending.length} von ${allPending.length}`;
|
||||||
pane.innerHTML = `
|
pane.innerHTML = `
|
||||||
<div class="card" style="margin-bottom:16px;">
|
<div class="card" style="margin-bottom:16px;">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h2>Vorschläge (${pending.length} offen)</h2>
|
<h2>Vorschläge (${headCount})</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table>
|
<table>
|
||||||
@@ -271,22 +317,40 @@ const MEDIA_TYPE_LABELS = {
|
|||||||
async function loadClassificationQueue() {
|
async function loadClassificationQueue() {
|
||||||
const list = document.getElementById("classificationReviewList");
|
const list = document.getElementById("classificationReviewList");
|
||||||
if (!list) return;
|
if (!list) return;
|
||||||
const minConf = parseFloat(document.getElementById("reviewMinConfidence")?.value || "0");
|
|
||||||
list.innerHTML = '<div class="text-muted" style="padding:24px;text-align:center;">Lade…</div>';
|
list.innerHTML = '<div class="text-muted" style="padding:24px;text-align:center;">Lade…</div>';
|
||||||
try {
|
try {
|
||||||
const items = await API.get(`/api/sources/classification/queue?limit=200&min_confidence=${minConf}`);
|
classificationQueueCache = await API.get("/api/sources/classification/queue?limit=200&min_confidence=0");
|
||||||
const countEl = document.getElementById("reviewPendingCount");
|
renderClassificationList();
|
||||||
if (countEl) countEl.textContent = String(items.length);
|
|
||||||
if (items.length === 0) {
|
|
||||||
list.innerHTML = '<div class="text-muted" style="padding:24px;text-align:center;">Keine ausstehenden Klassifikationen.</div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
list.innerHTML = items.map((it) => renderClassificationQueueItem(it)).join("");
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
list.innerHTML = `<div class="text-danger" style="padding:24px;text-align:center;">Fehler. ${esc(err.message)}</div>`;
|
list.innerHTML = `<div class="text-danger" style="padding:24px;text-align:center;">Fehler. ${esc(err.message)}</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wendet den Konfidenz-Checkbox-Filter clientseitig auf die geladene Queue an
|
||||||
|
function renderClassificationList() {
|
||||||
|
const list = document.getElementById("classificationReviewList");
|
||||||
|
if (!list) return;
|
||||||
|
const all = classificationQueueCache || [];
|
||||||
|
const f = aufgabenFilters.conf;
|
||||||
|
const items = f.size
|
||||||
|
? all.filter((it) => {
|
||||||
|
const c = (it.proposed && it.proposed.confidence) || 0;
|
||||||
|
return CONF_BUCKETS.some((b) => f.has(b.key) && b.test(c));
|
||||||
|
})
|
||||||
|
: all;
|
||||||
|
const countEl = document.getElementById("reviewPendingCount");
|
||||||
|
if (countEl) countEl.textContent = String(all.length);
|
||||||
|
if (all.length === 0) {
|
||||||
|
list.innerHTML = '<div class="text-muted" style="padding:24px;text-align:center;">Keine ausstehenden Klassifikationen.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (items.length === 0) {
|
||||||
|
list.innerHTML = '<div class="text-muted" style="padding:24px;text-align:center;">Keine Klassifikationen mit diesen Filtern.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
list.innerHTML = items.map((it) => renderClassificationQueueItem(it)).join("");
|
||||||
|
}
|
||||||
|
|
||||||
function renderClassificationQueueItem(item) {
|
function renderClassificationQueueItem(item) {
|
||||||
const cur = item.current || {};
|
const cur = item.current || {};
|
||||||
const prop = item.proposed || {};
|
const prop = item.proposed || {};
|
||||||
|
|||||||
@@ -5,8 +5,10 @@
|
|||||||
let globalSourcesCache = [];
|
let globalSourcesCache = [];
|
||||||
// Auswahl für Bulk-Promote (nur bei Herkunfts-Filter Kundenquellen sichtbar)
|
// Auswahl für Bulk-Promote (nur bei Herkunfts-Filter Kundenquellen sichtbar)
|
||||||
let tenantSelected = new Set();
|
let tenantSelected = new Set();
|
||||||
// Aktiver Herkunfts-Filter ("" = alle, "global", "tenant")
|
// Checkbox-Filter der Quellenliste. Leeres Set = keine Einschränkung.
|
||||||
let originFilter = "";
|
const unifiedFilters = { origin: new Set(), types: new Set(), cats: new Set(), status: new Set(), langs: new Set() };
|
||||||
|
let unifiedLangOptions = [];
|
||||||
|
let lastShowSelect = false;
|
||||||
// Lazy-Cache für die Health-Ausklapp-Details je Quelle
|
// Lazy-Cache für die Health-Ausklapp-Details je Quelle
|
||||||
const sourceHealthDetailCache = new Map();
|
const sourceHealthDetailCache = new Map();
|
||||||
|
|
||||||
@@ -85,11 +87,6 @@ function setupSourceSubTabs() {
|
|||||||
async function loadUnifiedSources() {
|
async function loadUnifiedSources() {
|
||||||
try {
|
try {
|
||||||
await ensureMeta();
|
await ensureMeta();
|
||||||
// Kategorien/Typen-Dropdowns aus META befüllen (idempotent)
|
|
||||||
if (window.META && window.META.categories && window.META.categories.length) {
|
|
||||||
populateSelect(document.getElementById("globalFilterCategory"), window.META.categories, "Alle Kategorien");
|
|
||||||
populateSelect(document.getElementById("globalFilterType"), window.META.types || [], "Alle Typen");
|
|
||||||
}
|
|
||||||
const [list, stats, languages] = await Promise.all([
|
const [list, stats, languages] = await Promise.all([
|
||||||
API.get("/api/sources?scope=all"),
|
API.get("/api/sources?scope=all"),
|
||||||
API.get("/api/sources/stats"),
|
API.get("/api/sources/stats"),
|
||||||
@@ -97,11 +94,7 @@ async function loadUnifiedSources() {
|
|||||||
]);
|
]);
|
||||||
globalSourcesCache = list;
|
globalSourcesCache = list;
|
||||||
sourceHealthDetailCache.clear();
|
sourceHealthDetailCache.clear();
|
||||||
populateSelect(
|
unifiedLangOptions = (languages || []).map(l => ({ key: l, label: l }));
|
||||||
document.getElementById("globalFilterLanguage"),
|
|
||||||
(languages || []).map(l => ({ key: l, label: l })),
|
|
||||||
"Alle Sprachen",
|
|
||||||
);
|
|
||||||
// datalist fuer Edit-Modal
|
// datalist fuer Edit-Modal
|
||||||
const dl = document.getElementById("languageSuggestions");
|
const dl = document.getElementById("languageSuggestions");
|
||||||
if (dl) {
|
if (dl) {
|
||||||
@@ -112,6 +105,7 @@ async function loadUnifiedSources() {
|
|||||||
dl.appendChild(o);
|
dl.appendChild(o);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
renderUnifiedFilters();
|
||||||
renderUnifiedStats(stats);
|
renderUnifiedStats(stats);
|
||||||
filterUnifiedSources();
|
filterUnifiedSources();
|
||||||
checkHealthRunOnLoad();
|
checkHealthRunOnLoad();
|
||||||
@@ -120,6 +114,24 @@ async function loadUnifiedSources() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Checkbox-Filterleiste der Quellenliste (Herkunft, Typ, Status, Kategorie, Sprache)
|
||||||
|
function renderUnifiedFilters() {
|
||||||
|
const bar = document.getElementById("unifiedFilterBar");
|
||||||
|
if (!bar) return;
|
||||||
|
bar.innerHTML = "";
|
||||||
|
renderFilterGroup(bar, "Herkunft", [
|
||||||
|
{ key: "global", label: "Grundquellen" },
|
||||||
|
{ key: "tenant", label: "Kundenquellen" },
|
||||||
|
], unifiedFilters.origin, filterUnifiedSources);
|
||||||
|
renderFilterGroup(bar, "Typ", (window.META && window.META.types) || [], unifiedFilters.types, filterUnifiedSources);
|
||||||
|
renderFilterGroup(bar, "Status", [
|
||||||
|
{ key: "active", label: "Aktiv" },
|
||||||
|
{ key: "inactive", label: "Inaktiv" },
|
||||||
|
], unifiedFilters.status, filterUnifiedSources);
|
||||||
|
renderFilterGroup(bar, "Kategorie", (window.META && window.META.categories) || [], unifiedFilters.cats, filterUnifiedSources);
|
||||||
|
renderFilterGroup(bar, "Sprache", unifiedLangOptions, unifiedFilters.langs, filterUnifiedSources);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async function showSourceAudit(sourceId, sourceName) {
|
async function showSourceAudit(sourceId, sourceName) {
|
||||||
@@ -338,7 +350,9 @@ function renderUnifiedSources(sources) {
|
|||||||
});
|
});
|
||||||
tbody.innerHTML = html;
|
tbody.innerHTML = html;
|
||||||
|
|
||||||
const originLabel = originFilter === "global" ? "Grundquellen" : originFilter === "tenant" ? "Kundenquellen" : "Quellen";
|
const o = unifiedFilters.origin;
|
||||||
|
const originLabel = (o.has("global") && !o.has("tenant")) ? "Grundquellen"
|
||||||
|
: (o.has("tenant") && !o.has("global")) ? "Kundenquellen" : "Quellen";
|
||||||
document.getElementById("globalSourceCount").textContent = `${sources.length} ${originLabel}`;
|
document.getElementById("globalSourceCount").textContent = `${sources.length} ${originLabel}`;
|
||||||
updateBulkButton();
|
updateBulkButton();
|
||||||
|
|
||||||
@@ -358,26 +372,19 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
|
|
||||||
function filterUnifiedSources() {
|
function filterUnifiedSources() {
|
||||||
const q = (document.getElementById("globalSourceSearch")?.value || "").toLowerCase();
|
const q = (document.getElementById("globalSourceSearch")?.value || "").toLowerCase();
|
||||||
const typeFilter = document.getElementById("globalFilterType")?.value || "";
|
const f = unifiedFilters;
|
||||||
const catFilter = document.getElementById("globalFilterCategory")?.value || "";
|
|
||||||
const statusFilter = document.getElementById("globalFilterStatus")?.value || "";
|
|
||||||
const langFilter = document.getElementById("globalFilterLanguage")?.value || "";
|
|
||||||
|
|
||||||
const newOrigin = document.getElementById("filterOrigin")?.value || "";
|
|
||||||
if (newOrigin !== originFilter) {
|
|
||||||
originFilter = newOrigin;
|
|
||||||
tenantSelected.clear();
|
|
||||||
}
|
|
||||||
updateOriginControls();
|
updateOriginControls();
|
||||||
|
|
||||||
let filtered = globalSourcesCache.filter((s) => {
|
let filtered = globalSourcesCache.filter((s) => {
|
||||||
if (originFilter === "global" && s.tenant_id != null) return false;
|
if (f.origin.size) {
|
||||||
if (originFilter === "tenant" && s.tenant_id == null) return false;
|
const isTenant = s.tenant_id != null;
|
||||||
|
if (!((isTenant && f.origin.has("tenant")) || (!isTenant && f.origin.has("global")))) return false;
|
||||||
|
}
|
||||||
if (q && !(s.name.toLowerCase().includes(q) || (s.domain || "").toLowerCase().includes(q) || (s.url || "").toLowerCase().includes(q) || (s.bias || "").toLowerCase().includes(q) || (s.org_name || "").toLowerCase().includes(q))) return false;
|
if (q && !(s.name.toLowerCase().includes(q) || (s.domain || "").toLowerCase().includes(q) || (s.url || "").toLowerCase().includes(q) || (s.bias || "").toLowerCase().includes(q) || (s.org_name || "").toLowerCase().includes(q))) return false;
|
||||||
if (typeFilter && s.source_type !== typeFilter) return false;
|
if (f.types.size && !f.types.has(s.source_type)) return false;
|
||||||
if (catFilter && s.category !== catFilter) return false;
|
if (f.cats.size && !f.cats.has(s.category)) return false;
|
||||||
if (statusFilter && s.status !== statusFilter) return false;
|
if (f.status.size && !f.status.has(s.status)) return false;
|
||||||
if (langFilter && s.language !== langFilter) return false;
|
if (f.langs.size && !f.langs.has(s.language)) return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -410,9 +417,15 @@ function sortUnifiedSources(field) {
|
|||||||
filterUnifiedSources();
|
filterUnifiedSources();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checkbox-Spalte + Bulk-Knopf nur bei Herkunfts-Filter "Kundenquellen"
|
// Checkbox-Spalte + Bulk-Knopf nur, wenn der Herkunfts-Filter ausschließlich
|
||||||
|
// auf Kundenquellen steht. Beim Verlassen wird die Auswahl geleert.
|
||||||
function updateOriginControls() {
|
function updateOriginControls() {
|
||||||
const showSelect = originFilter === "tenant";
|
const o = unifiedFilters.origin;
|
||||||
|
const showSelect = o.has("tenant") && !o.has("global");
|
||||||
|
if (!showSelect && lastShowSelect) {
|
||||||
|
tenantSelected.clear();
|
||||||
|
}
|
||||||
|
lastShowSelect = showSelect;
|
||||||
const table = document.getElementById("unifiedTable");
|
const table = document.getElementById("unifiedTable");
|
||||||
if (table) table.classList.toggle("show-select", showSelect);
|
if (table) table.classList.toggle("show-select", showSelect);
|
||||||
const btn = document.getElementById("tenantBulkPromoteBtn");
|
const btn = document.getElementById("tenantBulkPromoteBtn");
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren