Quellenverwaltung: Filter, Sortierung, Artikelzähler, Umlaute
- Filter-Dropdowns für Typ, Kategorie und Status - Sortierbare Spalten (Name, Domain, Typ, Kategorie, Artikel, Status) - Artikel-Spalte zeigt article_count an - Umlaute korrigiert (Löschen, Übernehmen, Läuft ab, Hinzugefügt von) - 53 kaputte/doppelte Quellen bereinigt (Bashinho, Netzpolitik, Facebook) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -309,6 +309,35 @@ input:focus, select:focus, textarea:focus {
|
||||
}
|
||||
|
||||
/* --- Tables --- */
|
||||
/* Filter-Selects */
|
||||
.filter-select {
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Sortierbare Spalten */
|
||||
th.sortable {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
th.sortable:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.sort-icon {
|
||||
font-size: 10px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@
|
||||
<th>Organisation</th>
|
||||
<th>Typ</th>
|
||||
<th>Max Nutzer</th>
|
||||
<th>Laeuft ab</th>
|
||||
<th>Läuft ab</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -205,8 +205,31 @@
|
||||
<!-- Grundquellen -->
|
||||
<div class="section active" id="sub-global-sources">
|
||||
<div class="action-bar">
|
||||
<div style="display:flex;align-items:center;gap:12px;">
|
||||
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
|
||||
<input type="text" class="search-input" id="globalSourceSearch" placeholder="Grundquelle suchen...">
|
||||
<select class="filter-select" id="globalFilterType" onchange="filterGlobalSources()">
|
||||
<option value="">Alle Typen</option>
|
||||
<option value="rss_feed">RSS-Feed</option>
|
||||
<option value="web_source">Webquelle</option>
|
||||
</select>
|
||||
<select class="filter-select" id="globalFilterCategory" onchange="filterGlobalSources()">
|
||||
<option value="">Alle Kategorien</option>
|
||||
<option value="nachrichtenagentur">Nachrichtenagentur</option>
|
||||
<option value="oeffentlich-rechtlich">Öffentlich-Rechtlich</option>
|
||||
<option value="qualitaetszeitung">Qualitätszeitung</option>
|
||||
<option value="behoerde">Behörde</option>
|
||||
<option value="fachmedien">Fachmedien</option>
|
||||
<option value="think-tank">Think-Tank</option>
|
||||
<option value="international">International</option>
|
||||
<option value="regional">Regional</option>
|
||||
<option value="boulevard">Boulevard</option>
|
||||
<option value="sonstige">Sonstige</option>
|
||||
</select>
|
||||
<select class="filter-select" id="globalFilterStatus" onchange="filterGlobalSources()">
|
||||
<option value="">Alle Status</option>
|
||||
<option value="active">Aktiv</option>
|
||||
<option value="inactive">Inaktiv</option>
|
||||
</select>
|
||||
<span class="text-secondary" id="globalSourceCount"></span>
|
||||
</div>
|
||||
<button class="btn btn-secondary" id="discoverSourceBtn">Erkennen</button>
|
||||
@@ -217,12 +240,13 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th class="sortable" data-sort="name" onclick="sortGlobalSources('name')">Name <span class="sort-icon"></span></th>
|
||||
<th>URL</th>
|
||||
<th>Domain</th>
|
||||
<th>Typ</th>
|
||||
<th>Kategorie</th>
|
||||
<th>Status</th>
|
||||
<th class="sortable" data-sort="domain" onclick="sortGlobalSources('domain')">Domain <span class="sort-icon"></span></th>
|
||||
<th class="sortable" data-sort="source_type" onclick="sortGlobalSources('source_type')">Typ <span class="sort-icon"></span></th>
|
||||
<th class="sortable" data-sort="category" onclick="sortGlobalSources('category')">Kategorie <span class="sort-icon"></span></th>
|
||||
<th class="sortable" data-sort="article_count" onclick="sortGlobalSources('article_count')">Artikel <span class="sort-icon"></span></th>
|
||||
<th class="sortable" data-sort="status" onclick="sortGlobalSources('status')">Status <span class="sort-icon"></span></th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -250,7 +274,7 @@
|
||||
<th>Typ</th>
|
||||
<th>Kategorie</th>
|
||||
<th>Organisation</th>
|
||||
<th>Hinzugefuegt von</th>
|
||||
<th>Hinzugefügt von</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
let globalSourcesCache = [];
|
||||
let tenantSourcesCache = [];
|
||||
let editingSourceId = null;
|
||||
let globalSortField = "name";
|
||||
let globalSortAsc = true;
|
||||
|
||||
const CATEGORY_LABELS = {
|
||||
nachrichtenagentur: "Nachrichtenagentur",
|
||||
@@ -21,7 +23,7 @@ const CATEGORY_LABELS = {
|
||||
const TYPE_LABELS = {
|
||||
rss_feed: "RSS-Feed",
|
||||
web_source: "Webquelle",
|
||||
excluded: "Gesperrt",
|
||||
excluded: "Ausgeschlossen",
|
||||
};
|
||||
|
||||
// --- Init ---
|
||||
@@ -63,7 +65,7 @@ async function loadGlobalSources() {
|
||||
function renderGlobalSources(sources) {
|
||||
const tbody = document.getElementById("globalSourceTable");
|
||||
if (sources.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="7" class="text-muted">Keine Grundquellen</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="8" class="text-muted">Keine Grundquellen</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = sources.map((s) => `
|
||||
@@ -73,31 +75,72 @@ function renderGlobalSources(sources) {
|
||||
<td>${esc(s.domain || "-")}</td>
|
||||
<td>${TYPE_LABELS[s.source_type] || s.source_type}</td>
|
||||
<td>${CATEGORY_LABELS[s.category] || s.category}</td>
|
||||
<td class="text-right">${s.article_count || 0}</td>
|
||||
<td><span class="badge badge-${s.status === "active" ? "active" : "inactive"}">${s.status === "active" ? "Aktiv" : "Inaktiv"}</span></td>
|
||||
<td>
|
||||
<button class="btn btn-secondary btn-small" onclick="editGlobalSource(${s.id})">Bearbeiten</button>
|
||||
<button class="btn btn-danger btn-small" onclick="confirmDeleteGlobalSource(${s.id}, '${esc(s.name)}')">Loeschen</button>
|
||||
<button class="btn btn-danger btn-small" onclick="confirmDeleteGlobalSource(${s.id}, '${esc(s.name)}')">Löschen</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join("");
|
||||
|
||||
document.getElementById("globalSourceCount").textContent = `${sources.length} Grundquellen`;
|
||||
|
||||
// Sort-Icons aktualisieren
|
||||
document.querySelectorAll("th.sortable .sort-icon").forEach(el => el.textContent = "");
|
||||
const activeHeader = document.querySelector(`th.sortable[data-sort="${globalSortField}"] .sort-icon`);
|
||||
if (activeHeader) activeHeader.textContent = globalSortAsc ? " ▲" : " ▼";
|
||||
}
|
||||
|
||||
// Suche
|
||||
// Filter + Sortierung
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const el = document.getElementById("globalSourceSearch");
|
||||
if (el) {
|
||||
el.addEventListener("input", () => {
|
||||
const q = el.value.toLowerCase();
|
||||
const filtered = globalSourcesCache.filter((s) =>
|
||||
s.name.toLowerCase().includes(q) || (s.domain || "").toLowerCase().includes(q) || (s.category || "").toLowerCase().includes(q)
|
||||
);
|
||||
renderGlobalSources(filtered);
|
||||
});
|
||||
el.addEventListener("input", () => filterGlobalSources());
|
||||
}
|
||||
});
|
||||
|
||||
function filterGlobalSources() {
|
||||
const q = (document.getElementById("globalSourceSearch")?.value || "").toLowerCase();
|
||||
const typeFilter = document.getElementById("globalFilterType")?.value || "";
|
||||
const catFilter = document.getElementById("globalFilterCategory")?.value || "";
|
||||
const statusFilter = document.getElementById("globalFilterStatus")?.value || "";
|
||||
|
||||
let filtered = globalSourcesCache.filter((s) => {
|
||||
if (q && !(s.name.toLowerCase().includes(q) || (s.domain || "").toLowerCase().includes(q) || (s.url || "").toLowerCase().includes(q))) return false;
|
||||
if (typeFilter && s.source_type !== typeFilter) return false;
|
||||
if (catFilter && s.category !== catFilter) return false;
|
||||
if (statusFilter && s.status !== statusFilter) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
// Sortierung anwenden
|
||||
filtered.sort((a, b) => {
|
||||
let va = a[globalSortField] ?? "";
|
||||
let vb = b[globalSortField] ?? "";
|
||||
if (globalSortField === "article_count") {
|
||||
va = va || 0; vb = vb || 0;
|
||||
return globalSortAsc ? va - vb : vb - va;
|
||||
}
|
||||
va = String(va).toLowerCase();
|
||||
vb = String(vb).toLowerCase();
|
||||
const cmp = va.localeCompare(vb, "de");
|
||||
return globalSortAsc ? cmp : -cmp;
|
||||
});
|
||||
|
||||
renderGlobalSources(filtered);
|
||||
}
|
||||
|
||||
function sortGlobalSources(field) {
|
||||
if (globalSortField === field) {
|
||||
globalSortAsc = !globalSortAsc;
|
||||
} else {
|
||||
globalSortField = field;
|
||||
globalSortAsc = true;
|
||||
}
|
||||
filterGlobalSources();
|
||||
}
|
||||
|
||||
// --- Grundquelle erstellen/bearbeiten ---
|
||||
function openNewGlobalSource() {
|
||||
editingSourceId = null;
|
||||
@@ -172,7 +215,7 @@ function setupSourceForms() {
|
||||
|
||||
function confirmDeleteGlobalSource(id, name) {
|
||||
showConfirm(
|
||||
"Grundquelle loeschen",
|
||||
"Grundquelle löschen",
|
||||
`Soll die Grundquelle "${name}" endgültig gelöscht werden? Sie wird für alle Monitore entfernt.`,
|
||||
async () => {
|
||||
try {
|
||||
@@ -210,7 +253,7 @@ function renderTenantSources(sources) {
|
||||
<td>${esc(s.org_name || "-")}</td>
|
||||
<td>${esc(s.added_by || "-")}</td>
|
||||
<td>
|
||||
<button class="btn btn-primary btn-small" onclick="promoteSource(${s.id}, '${esc(s.name)}')">Uebernehmen</button>
|
||||
<button class="btn btn-primary btn-small" onclick="promoteSource(${s.id}, '${esc(s.name)}')">Übernehmen</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join("");
|
||||
@@ -320,7 +363,7 @@ async function addDiscoveredFeeds() {
|
||||
});
|
||||
|
||||
if (selected.length === 0) {
|
||||
alert("Keine Feeds ausgewaehlt");
|
||||
alert("Keine Feeds ausgewählt");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren