Verwaltungsseite perfektioniert: Team-Verwaltung für Modis/Scouts/Creator/Manager

Nutzer-Wunsch 21.08.2026: "überall wo Modis sind oder Scouts oder
Manager, oder Creator, will ich dass ich die easy über meine
Verwaltungsseite hinzufüge und die automatisch in der Website
hinzugefügt werden ... auch mit Fotos und TikTok Link."

Backend (server-internal, braucht Filipes manuellen Deploy):
- Neue Tabelle team_members (Migration 0007) für alle vier Kategorien
  gemeinsam -- ergänzt, überschreibt NIE die von Hand gepflegten
  Einträge in data-modis.js/data-scouts.js/data-creator.js.
- routes/team.js: öffentliches Lesen (nur aktive Mitglieder, optional
  nach Kategorie gefiltert), TEAM_MANAGE-geschütztes Anlegen/Bearbeiten/
  Löschen, Foto-Upload (gleiches Muster wie "Event des Jahres"),
  automatische Übersetzung von Rolle/Bio/Zitat wie bei anderen
  admin-gepflegten Texten. Slug global eindeutig (mit automatischer
  Kollisionsauflösung), TikTok-Kurzname wird zu voller URL ergänzt.
  27 Tests in isolierter Testumgebung geprüft (kein better-sqlite3
  lokal kompilierbar).

Frontend:
- window.dogiTeamLaden() in main.js: holt admin-gepflegte Mitglieder
  einer (oder aller) Kategorien und bringt sie in exakt dieselbe Form
  wie die bestehenden data-*.js-Einträge.
- team-scouts.html/team-modis.html/creator.html hängen das Ergebnis
  einfach an ihre bestehenden Arrays an und rendern erneut -- exakt
  derselbe Look wie die schon bestehenden Profile (Foto/Avatar-Initiale,
  Rolle, Zitat, TikTok-Button). creator.html hatte bisher ein "return"
  bei leerem CREATORS-Array, wodurch admin-Creator NIE geladen worden
  wären -- gefixt. team-modis.html berücksichtigt Rang/Rang-Bezeichnung
  für die Pyramide.
- Neue Sektion "Weitere Manager" auf manager.html, komplett unsichtbar
  bis der erste Manager angelegt wird (data-manager.js neu, wie
  data-creator.js aktuell leer).
- profil.html sucht jetzt kategorieübergreifend auch in admin-gepflegten
  Profilen, inkl. korrektem Theme/Zurück-Link auch für Manager.

Verwaltung: neue Sektion "Team verwalten" (TEAM_MANAGE-Berechtigung) --
ein Formular für alle vier Kategorien mit Foto-Sofort-Upload,
TikTok-Feld, Bio/Zitat, bedingten Rang-Feldern (nur Modi), Kategorie-
Filterleiste und Bearbeiten/Löschen pro Eintrag.

Beim Testen mit Playwright einen echten Syntaxfehler gefunden und
gefixt (ASCII-Anführungszeichen statt schließendem „" in einem
Statustext), der das GESAMTE Verwaltungs-Skript und damit die komplette
Seite lahmgelegt hätte.

Cache-Busting-Version auf 20260821j erhöht.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-08-21 03:06:21 +02:00
co-authored by Claude Sonnet 5
parent 8ead1b750b
commit 7060a4a6ae
45 changed files with 1077 additions and 247 deletions
+317 -6
View File
@@ -19,10 +19,10 @@
<link rel="apple-touch-icon" href="assets/img/apple-touch-icon-admin.png" />
<meta name="apple-mobile-web-app-title" content="DogiCrew-Verwaltung" />
<meta name="theme-color" content="#0e0a16" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821j" />
<style>
/* =====================================================================
verwaltung.html — Dogis privater Command-Center-Bereich.
@@ -858,6 +858,109 @@
</div>
</section>
<!-- ===== "Team verwalten" (21.08.2026) — Nutzer-Wunsch: "überall wo
Modis sind oder Scouts oder Manager, oder Creator, will ich dass ich
die easy über meine Verwaltungsseite hinzufüge und die automatisch
in der Website hinzugefügt werden ... auch mit Fotos und TikTok
Link". Ergänzt (überschreibt NICHT) die von Hand gepflegten
Einträge in assets/js/data-modis.js/data-scouts.js/data-creator.js/
data-manager.js -- neue Mitglieder erscheinen automatisch auf
team-modis.html/team-scouts.html/creator.html/manager.html UND auf
profil.html, exakt im selben Look wie die schon bestehenden (siehe
window.dogiTeamLaden() in main.js). Rolle/Bio/Zitat werden wie
"Event des Jahres" nur auf Deutsch eingetippt und beim Speichern
automatisch übersetzt. -->
<section class="section-tight" id="team-root" style="display:none;padding-top:0;">
<div class="container">
<h2 class="text-center" style="margin-bottom:.3rem;">🧑‍🤝‍🧑 Team verwalten</h2>
<p class="small muted text-center" style="max-width:64ch;margin:0 auto 1.4rem;">
Modis, Scouts, Creator und Manager — neue Mitglieder erscheinen automatisch auf der
passenden Seite (mit eigenem Profil, Foto und TikTok-Button), genau wie die schon
bestehenden. Rolle/Vorstellung/Zitat bitte auf Deutsch eintippen, der Rest wird beim
Speichern automatisch übersetzt.
</p>
<div class="card vw-card vw-shine-card vw-noise" style="max-width:640px;margin:0 auto 2rem;padding:1.5rem;">
<h3 id="team-form-titel" style="margin-top:0;">Neues Mitglied</h3>
<input type="hidden" id="team-id" value="" />
<div class="field">
<label for="team-kategorie">Kategorie</label>
<select id="team-kategorie">
<option value="modi">Modi (Team Dogi)</option>
<option value="scout">Scout (Spicy Media)</option>
<option value="creator">Creator (Spicy Media)</option>
<option value="manager">Manager (Spicy Media)</option>
</select>
</div>
<div class="field">
<label for="team-name">Name</label>
<input type="text" id="team-name" maxlength="60" placeholder="z. B. Foxy" />
</div>
<div class="field">
<label for="team-rolle">Rolle (Deutsch)</label>
<input type="text" id="team-rolle" maxlength="120" placeholder="z. B. Scout (Manager) — Rechte Hand" />
</div>
<div style="margin-bottom:1rem;text-align:center;">
<img id="team-foto-vorschau" alt="" style="width:140px;height:140px;object-fit:cover;border-radius:50%;background:var(--bg-alt);display:none;" />
</div>
<div class="field">
<label for="team-foto-input">Foto (optional, max. 5 MB, JPG/PNG/WebP)</label>
<input type="file" id="team-foto-input" accept="image/jpeg,image/png,image/webp" />
</div>
<div class="field">
<label for="team-tiktok">TikTok (Benutzername oder Link, optional)</label>
<input type="text" id="team-tiktok" maxlength="200" placeholder="z. B. @foxy123 oder https://tiktok.com/@foxy123" />
</div>
<div class="field">
<label for="team-bio">Kurze Vorstellung (Deutsch, optional)</label>
<textarea id="team-bio" maxlength="800" rows="3"></textarea>
</div>
<div class="field">
<label for="team-zitat">Zitat/Motto (Deutsch, optional)</label>
<input type="text" id="team-zitat" maxlength="200" />
</div>
<div id="team-rang-felder" style="display:none;">
<div class="grid grid-2" style="gap:.8rem;">
<div class="field">
<label for="team-rang">Rang (Pyramiden-Stufe)</label>
<input type="number" id="team-rang" min="1" max="99" />
</div>
<div class="field">
<label for="team-rang-label">Rang-Bezeichnung (Deutsch)</label>
<input type="text" id="team-rang-label" maxlength="40" placeholder="z. B. Rechte Hand" />
</div>
</div>
<p class="small muted" style="margin:.3rem 0 1rem;">
Bestimmt die Ebene in der Modi-Pyramide — ohne Rang landet die Person automatisch
ganz unten.
</p>
</div>
<div class="field">
<label for="team-reihenfolge">Reihenfolge (kleinere Zahl erscheint zuerst)</label>
<input type="number" id="team-reihenfolge" value="0" />
</div>
<label style="display:flex;align-items:center;gap:.6rem;margin:1rem 0;cursor:pointer;">
<input type="checkbox" id="team-aktiv" checked style="width:1.2rem;height:1.2rem;" />
<span>Aktiv — öffentlich sichtbar</span>
</label>
<div class="btn-row">
<button type="button" class="btn btn-primary" id="team-speichern" style="flex:1;">Speichern</button>
<button type="button" class="btn btn-outline" id="team-abbrechen" style="display:none;">Abbrechen</button>
</div>
<p class="small" id="team-status" style="margin-top:.6rem;"></p>
</div>
<div class="btn-row" style="justify-content:center;margin-bottom:1.4rem;" id="team-filter-bar">
<button type="button" class="filter-btn active" data-team-filter="alle">Alle</button>
<button type="button" class="filter-btn" data-team-filter="modi">Modi</button>
<button type="button" class="filter-btn" data-team-filter="scout">Scouts</button>
<button type="button" class="filter-btn" data-team-filter="creator">Creator</button>
<button type="button" class="filter-btn" data-team-filter="manager">Manager</button>
</div>
<div id="team-liste"></div>
</div>
</section>
<!-- ===== Supporter-Liste ===== -->
<section class="section-tight" id="supp-root" style="display:none;padding-top:0;">
<div class="container" id="liste-ansicht">
@@ -1032,8 +1135,8 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/main.js?v=20260821i"></script>
<script src="assets/js/admin-auth.js?v=20260821i"></script>
<script src="assets/js/main.js?v=20260821j"></script>
<script src="assets/js/admin-auth.js?v=20260821j"></script>
<script>
(function () {
const API_BASE_URL = "https://postfach.dogfather-universe.com";
@@ -1113,6 +1216,10 @@
document.getElementById("testimonials-root").style.display = "";
await ladeUndRenderTestimonials();
}
if (window.AdminAuth.sessionHasPermission(session, "TEAM_MANAGE")) {
document.getElementById("team-root").style.display = "";
await ladeUndRenderTeam();
}
const pollsBox = document.getElementById("box-abstimmungen");
if (window.AdminAuth.sessionHasPermission(session, "POLLS_MANAGE")) {
pollsBox.style.display = "";
@@ -1132,6 +1239,7 @@
document.getElementById("events-root").style.display = "none";
document.getElementById("special-event-root").style.display = "none";
document.getElementById("testimonials-root").style.display = "none";
document.getElementById("team-root").style.display = "none";
document.getElementById("box-abstimmungen").style.display = "none";
}
@@ -1548,6 +1656,209 @@
});
}
/* ===== "Team verwalten" — Modis/Scouts/Creator/Manager (21.08.2026) =====
Ein Formular für alle vier Kategorien (identische Felder), darunter
eine nach Kategorie filterbare Liste aller bereits angelegten
Mitglieder mit Bearbeiten/Löschen. Rolle/Bio/Zitat werden beim
Speichern serverseitig automatisch übersetzt (siehe routes/team.js) --
genau wie bei "Event des Jahres". Ein hochgeladenes Foto landet SOFORT
bei Auswahl auf dem Server (nicht erst bei "Speichern"), gleiches
Muster wie bei den Event-Bildern weiter oben. */
const TEAM_KATEGORIE_LABELS = { modi: "Modi", scout: "Scout", creator: "Creator", manager: "Manager" };
let teamHochgeladenesFoto = null;
let teamAktiverFilter = "alle";
function teamKarteHtml(m) {
const fotoHtml = m.foto
? `<img src="${m.foto}" alt="${escapeHtml(m.name)}" style="width:56px;height:56px;border-radius:50%;object-fit:cover;flex-shrink:0;">`
: `<span style="width:56px;height:56px;border-radius:50%;background:var(--bg-alt);display:flex;align-items:center;justify-content:center;font-weight:700;flex-shrink:0;">${escapeHtml((m.name || "?").charAt(0).toUpperCase())}</span>`;
return `<div class="card vw-card vw-shine-card" style="margin-bottom:1rem;padding:1.1rem 1.3rem;display:flex;gap:1rem;align-items:center;flex-wrap:wrap;" data-team-id="${m.id}">
${fotoHtml}
<div style="flex:1;min-width:180px;">
<strong>${escapeHtml(m.name)}</strong>
<span class="vw-overview-chip">${TEAM_KATEGORIE_LABELS[m.kategorie] || m.kategorie}</span>
<span class="vw-overview-chip" style="${m.status === "pausiert" ? "opacity:.6;" : ""}">${m.status === "pausiert" ? "⚪ Pausiert" : "🟢 Aktiv"}</span>
<div class="small" style="color:var(--text-muted);">${escapeHtml(m.rolle?.de || "")}</div>
${m.tiktok ? `<a href="${escapeHtml(m.tiktok)}" target="_blank" rel="noopener" class="small">♪ TikTok</a>` : ""}
</div>
<div class="btn-row" style="gap:.5rem;">
<button type="button" class="vw-refresh-btn team-bearbeiten-btn">Bearbeiten</button>
<button type="button" class="vw-refresh-btn team-loeschen-btn" style="color:#ff8a8a;">Löschen</button>
</div>
</div>`;
}
let teamAlleMitglieder = [];
function teamListeZeichnen() {
const listeEl = document.getElementById("team-liste");
const gefiltert = teamAktiverFilter === "alle" ? teamAlleMitglieder : teamAlleMitglieder.filter((m) => m.kategorie === teamAktiverFilter);
listeEl.innerHTML = gefiltert.length ? gefiltert.map(teamKarteHtml).join("") : `<p class="vw-leer">Noch keine Mitglieder in dieser Kategorie.</p>`;
listeEl.querySelectorAll(".team-bearbeiten-btn").forEach((btn) => {
btn.addEventListener("click", () => {
const id = btn.closest("[data-team-id]").dataset.teamId;
const m = teamAlleMitglieder.find((x) => x.id === id);
if (m) teamFormularBefuellen(m);
});
});
listeEl.querySelectorAll(".team-loeschen-btn").forEach((btn) => {
btn.addEventListener("click", async () => {
const id = btn.closest("[data-team-id]").dataset.teamId;
const m = teamAlleMitglieder.find((x) => x.id === id);
if (!confirm(`${m?.name || "Dieses Mitglied"} unwiderruflich löschen?`)) return;
btn.disabled = true;
await window.AdminAuth.apiCall(API_BASE_URL, "/admin/team/delete", { id });
await ladeUndRenderTeam();
});
});
}
async function ladeUndRenderTeam() {
const res = await window.AdminAuth.apiCall(API_BASE_URL, "/admin/team/list", {});
teamAlleMitglieder = res.ok ? (res.mitglieder || []) : [];
teamListeZeichnen();
}
function teamFormularZuruecksetzen() {
document.getElementById("team-id").value = "";
document.getElementById("team-form-titel").textContent = "Neues Mitglied";
document.getElementById("team-kategorie").value = "modi";
document.getElementById("team-name").value = "";
document.getElementById("team-rolle").value = "";
document.getElementById("team-tiktok").value = "";
document.getElementById("team-bio").value = "";
document.getElementById("team-zitat").value = "";
document.getElementById("team-rang").value = "";
document.getElementById("team-rang-label").value = "";
document.getElementById("team-reihenfolge").value = "0";
document.getElementById("team-aktiv").checked = true;
document.getElementById("team-foto-vorschau").style.display = "none";
document.getElementById("team-foto-input").value = "";
document.getElementById("team-abbrechen").style.display = "none";
document.getElementById("team-status").textContent = "";
teamHochgeladenesFoto = null;
teamRangFelderAnpassen();
}
function teamFormularBefuellen(m) {
document.getElementById("team-id").value = m.id;
document.getElementById("team-form-titel").textContent = `„${m.name}“ bearbeiten`;
document.getElementById("team-kategorie").value = m.kategorie;
document.getElementById("team-name").value = m.name;
document.getElementById("team-rolle").value = m.rolle?.de || "";
document.getElementById("team-tiktok").value = m.tiktok || "";
document.getElementById("team-bio").value = m.bio?.de || "";
document.getElementById("team-zitat").value = m.zitat?.de || "";
document.getElementById("team-rang").value = m.rang || "";
document.getElementById("team-rang-label").value = m.rangLabel?.de || "";
document.getElementById("team-reihenfolge").value = m.reihenfolge ?? 0;
document.getElementById("team-aktiv").checked = m.status !== "pausiert";
teamHochgeladenesFoto = m.foto || null;
const vorschau = document.getElementById("team-foto-vorschau");
if (teamHochgeladenesFoto) {
vorschau.src = teamHochgeladenesFoto;
vorschau.style.display = "";
} else {
vorschau.style.display = "none";
}
document.getElementById("team-abbrechen").style.display = "";
document.getElementById("team-status").textContent = "";
teamRangFelderAnpassen();
document.getElementById("team-root").scrollIntoView({ behavior: "smooth", block: "start" });
}
function teamRangFelderAnpassen() {
const istModi = document.getElementById("team-kategorie").value === "modi";
document.getElementById("team-rang-felder").style.display = istModi ? "" : "none";
}
document.getElementById("team-kategorie").addEventListener("change", teamRangFelderAnpassen);
document.getElementById("team-abbrechen").addEventListener("click", teamFormularZuruecksetzen);
document.getElementById("team-foto-input").addEventListener("change", async () => {
const input = document.getElementById("team-foto-input");
const datei = input.files?.[0];
if (!datei) return;
const statusEl = document.getElementById("team-status");
statusEl.textContent = "Foto wird hochgeladen …";
statusEl.style.color = "";
const form = new FormData();
form.append("image", datei);
const session = window.AdminAuth.getSession();
try {
const res = await fetch(`${API_BASE_URL}/admin/team/upload-photo`, {
method: "POST",
headers: session?.token ? { Authorization: `Bearer ${session.token}` } : {},
body: form,
});
const data = await res.json();
if (!data.ok) {
statusEl.textContent = data.error || "Foto-Upload fehlgeschlagen.";
statusEl.style.color = "#ff6b6b";
return;
}
teamHochgeladenesFoto = data.url;
const vorschau = document.getElementById("team-foto-vorschau");
vorschau.src = data.url;
vorschau.style.display = "";
statusEl.textContent = "Foto hochgeladen — jetzt noch auf „Speichern“ klicken.";
} catch {
statusEl.textContent = "Foto-Upload fehlgeschlagen (keine Verbindung).";
statusEl.style.color = "#ff6b6b";
}
});
document.getElementById("team-speichern").addEventListener("click", async () => {
const statusEl = document.getElementById("team-status");
const name = document.getElementById("team-name").value.trim();
const rolle = document.getElementById("team-rolle").value.trim();
if (!name || !rolle) {
statusEl.textContent = "Name und Rolle sind Pflicht.";
statusEl.style.color = "#ff6b6b";
return;
}
const btn = document.getElementById("team-speichern");
btn.disabled = true;
statusEl.textContent = "Wird gespeichert und übersetzt …";
statusEl.style.color = "";
const id = document.getElementById("team-id").value || undefined;
const res = await window.AdminAuth.apiCall(API_BASE_URL, "/admin/team/save", {
id,
kategorie: document.getElementById("team-kategorie").value,
name,
rolle,
foto: teamHochgeladenesFoto,
tiktok: document.getElementById("team-tiktok").value.trim(),
bio: document.getElementById("team-bio").value.trim(),
zitat: document.getElementById("team-zitat").value.trim(),
rang: document.getElementById("team-rang").value,
rangLabel: document.getElementById("team-rang-label").value.trim(),
reihenfolge: document.getElementById("team-reihenfolge").value,
status: document.getElementById("team-aktiv").checked ? "aktiv" : "pausiert",
});
btn.disabled = false;
if (!res.ok) {
statusEl.textContent = res.error || "Speichern fehlgeschlagen.";
statusEl.style.color = "#ff6b6b";
return;
}
statusEl.textContent = "✅ Gespeichert.";
statusEl.style.color = "#baf7d6";
teamFormularZuruecksetzen();
await ladeUndRenderTeam();
});
document.getElementById("team-filter-bar").addEventListener("click", (e) => {
const btn = e.target.closest(".filter-btn");
if (!btn) return;
teamAktiverFilter = btn.dataset.teamFilter;
document.querySelectorAll("#team-filter-bar .filter-btn").forEach((b) => b.classList.remove("active"));
btn.classList.add("active");
teamListeZeichnen();
});
teamRangFelderAnpassen();
// Nutzer-Wunsch 04.08.2026 (6. Runde): zwei getrennte Kisten/Formulare
// (Dogi + VanVan) statt einem gemeinsamen Feld — beide rufen denselben
// Login-Endpunkt auf, die Fehlermeldung erscheint jeweils nur in der