Wichtigster Befund vorab: Es gab noch KEINEN PDF-Bereich. Weder auf der Website (28 Seiten, keine davon Anleitungen) noch auf dem Server (null PDFs). Es wurde also nichts umstrukturiert, sondern neu gebaut -- und damit war die erste Frage nicht "wie sieht es aus", sondern "wie kommen die PDFs spaeter rein". Eine feste Liste im Code waere nach drei Monaten unbrauchbar gewesen. Filipe hat entschieden: eigener Pflegebereich, und alles nur fuer das Team. Die Bibliothek liegt deshalb komplett im Workspace hinter dem Login, nicht auf der oeffentlichen Seite. GLIEDERUNG Zwanzig Kategorien woertlich nach Vorgabe, gebuendelt in sechs Welten: TikTok, Streaming einrichten, Bild & Ton, Uebertragung & Aussehen, Inhalt & Menschen, Hilfe & Schnellstart. Zwanzig gleich aussehende Kacheln untereinander findet niemand -- gebuendelt sucht man erst die Welt, und darin stehen nur noch drei bis vier. Jede Welt hat ihre eigene Farbe, jede Kategorie ihr eigenes Symbol (20 verschiedene, keins doppelt). Die Kategorietexte sind Filipes Beschreibungen -- sie beantworten beim Einsortieren die einzige Frage, die man wirklich hat. Die Gliederung steht im Code, nicht in der Datenbank: Sie ist eine bewusste Ordnung, keine Nutzdaten. JEDE PDF HAT GENAU EINE HAUPTKATEGORIE, alles Weitere laeuft ueber Tags -- so gibt es jede Anleitung nur einmal, sie ist aber ueber mehrere Begriffe auffindbar. Genau wie gefordert. SUCHE UND FILTER Suche ueber Titel, Beschreibung, Tags und Dateiname. Filter nach Stufe (Einsteiger / Fortgeschritten / Profi), Geraet und Tag. Zwei Details: - "PC" zeigt auch die Anleitungen, die fuer BEIDE Geraete gelten -- sonst filtert man sich versehentlich die Haelfte weg. - Der Tag-Filter sucht mit Kommas drumherum, sonst wuerde "pc" auch bei "pc-spiele" anschlagen. Sobald gesucht oder gefiltert wird, verschwinden die Kacheln und es erscheinen Treffer. Wer sucht, will nicht erst noch klicken. Der Zustand steht in der Adresse (?k=obs&tag=...). Damit funktionieren Zurueck-Taste, Neuladen und Weiterschicken. Eine Kategorie, die man niemandem verlinken kann, ist keine Seite. Geprueft. SICHERHEIT Hochgeladen wird nur, was WIRKLICH eine PDF ist -- geprueft an den ersten Bytes (%PDF-), nicht an der Dateiendung. Die kann jeder umbenennen. Geprueft mit einer als .pdf getarnten HTML-Datei mit Skript: abgewiesen. Nur weil diese Pruefung existiert, darf die Datei ueberhaupt im Browser angezeigt werden statt bloss heruntergeladen -- und selbst dann mit strenger CSP und nosniff. Lesen darf jeder Angemeldete, pflegen nur DogFather. Geprueft: Creator und Scout bekommen 403 beim Hochladen und sehen keine Bearbeiten-Knoepfe. Ohne Anmeldung ist auch die Datei selbst nicht erreichbar (401). Bricht das Anlegen nach dem Schreiben ab, wird die Datei wieder geloescht -- sonst laege sie fuer immer verwaist auf der Platte. Beim Bearbeiten wird die Datei bewusst NICHT ersetzt. Wer eine neue Fassung hat, stellt sie neu ein. So bleibt nachvollziehbar, was wann galt. Geprueft: 20 Kacheln, 20 verschiedene Symbole, Kategorie oeffnen, Suche, Tag-Klick, Browser-Zurueck, Handy ohne Ueberlauf, 0 Konsolenfehler.
193 lines
8.5 KiB
JavaScript
193 lines
8.5 KiB
JavaScript
/* ===================================================================
|
||
Startseite nach dem Anmelden.
|
||
Die Seite ist bereits serverseitig geschützt (workspace.js). Dieses
|
||
Skript holt nur noch, WER angemeldet ist, und regelt das Abmelden.
|
||
=================================================================== */
|
||
(() => {
|
||
'use strict';
|
||
|
||
const ROLLENTEXT = {
|
||
admin: 'DogFather · Gesamtübersicht und Freigaben',
|
||
creator: 'Creator · dein eigener Bereich',
|
||
scout: 'Scout · deine Pipeline und die Creator, die du betreust',
|
||
};
|
||
|
||
/* Bereiche aus dem Konzept, mit der Phase aus dem Umsetzungsplan.
|
||
`rollen` steuert, wem ein Bereich überhaupt angezeigt wird - schon
|
||
jetzt, damit die Trennung von Anfang an sichtbar ist. */
|
||
const BEREICHE = [
|
||
{ name: 'Dashboard', phase: 1, rollen: ['admin', 'creator', 'scout'] },
|
||
{ name: 'Personen & Zugänge', phase: 1, rollen: ['admin'], ziel: 'personen.html' },
|
||
{ name: 'Creator-Profile', phase: 1, rollen: ['admin', 'scout'], ziel: 'profil.html' },
|
||
{ name: 'Mein Profil', phase: 1, rollen: ['creator'], ziel: 'profil.html' },
|
||
{ name: 'Start-Check', phase: 1, rollen: ['admin', 'creator', 'scout'], ziel: 'startcheck.html' },
|
||
{ name: 'Wissens-Bibliothek', phase: 1, rollen: ['admin', 'creator', 'scout'], ziel: 'wissen.html' },
|
||
{ name: 'Aufgaben', phase: 1, rollen: ['admin', 'creator', 'scout'], ziel: 'aufgaben.html' },
|
||
{ name: 'Kalender', phase: 1, rollen: ['admin', 'creator', 'scout'], ziel: 'kalender.html' },
|
||
{ name: 'Dateien', phase: 1, rollen: ['admin', 'creator', 'scout'], ziel: 'dateien.html' },
|
||
{ name: 'LIVE-Analyse', phase: 2, rollen: ['admin', 'creator', 'scout'], ziel: 'bereich.html?b=live' },
|
||
{ name: 'Content-Planung', phase: 2, rollen: ['admin', 'creator', 'scout'], ziel: 'bereich.html?b=content' },
|
||
{ name: 'Technik', phase: 2, rollen: ['admin', 'creator', 'scout'], ziel: 'bereich.html?b=technik' },
|
||
{ name: 'Community', phase: 2, rollen: ['admin', 'creator', 'scout'], ziel: 'bereich.html?b=community' },
|
||
{ name: 'Schutz & Regeln', phase: 2, rollen: ['admin', 'creator', 'scout'], ziel: 'bereich.html?b=schutz' },
|
||
{ name: 'Reports & Review', phase: 2, rollen: ['admin', 'creator', 'scout'], ziel: 'report.html' },
|
||
{ name: 'Calls & Protokolle', phase: 3, rollen: ['admin', 'creator', 'scout'], ziel: 'calls.html' },
|
||
{ name: 'Scout-Pipeline', phase: 3, rollen: ['admin', 'scout'], ziel: 'scouting.html' },
|
||
{ name: 'Automationen', phase: 4, rollen: ['admin'] },
|
||
];
|
||
|
||
const $ = (id) => document.getElementById(id);
|
||
|
||
async function laden() {
|
||
let ich;
|
||
try {
|
||
const antwort = await fetch('/workspace/api/ich', { credentials: 'same-origin' });
|
||
if (!antwort.ok) { location.assign('/workspace/'); return; }
|
||
ich = await antwort.json();
|
||
} catch {
|
||
location.assign('/workspace/');
|
||
return;
|
||
}
|
||
|
||
$('wer').textContent = `${ich.name} · ${ich.rolle_name || ich.rolle}`;
|
||
$('gruss').textContent = `Willkommen, ${ich.name}`;
|
||
$('rollentext').textContent = ROLLENTEXT[ich.rolle] || ich.rolle;
|
||
|
||
const liste = $('bereiche');
|
||
for (const b of BEREICHE.filter((b) => b.rollen.includes(ich.rolle))) {
|
||
const li = document.createElement('li');
|
||
li.className = 'bereich';
|
||
li.dataset.phase = String(b.phase);
|
||
if (b.ziel) li.dataset.fertig = 'ja';
|
||
|
||
const name = document.createElement('span');
|
||
name.className = 'bereich__name';
|
||
name.textContent = b.name;
|
||
|
||
const stand = document.createElement('span');
|
||
stand.className = 'bereich__stand';
|
||
stand.textContent = b.ziel ? 'öffnen' : (b.phase === 1 ? 'als Nächstes' : `Phase ${b.phase}`);
|
||
|
||
if (b.ziel) {
|
||
/* Fertige Bereiche sind echte Verweise -- Tastatur und
|
||
Rechtsklick funktionieren damit wie gewohnt. */
|
||
const a = document.createElement('a');
|
||
a.className = 'bereich__link';
|
||
a.href = b.ziel;
|
||
a.append(name, stand);
|
||
li.append(a);
|
||
} else {
|
||
li.append(name, stand);
|
||
}
|
||
liste.append(li);
|
||
}
|
||
|
||
await hinweiseLaden();
|
||
await zahlenLaden();
|
||
}
|
||
|
||
/* Was ist dran? Erster Baustein der Automationen aus dem Konzept.
|
||
Fuehrt bewusst keine eigenen Daten -- jeder Hinweis ist nur eine
|
||
Sicht auf etwas, das schon da ist, und verschwindet von selbst,
|
||
sobald die Sache erledigt ist. Nichts zu quittieren. */
|
||
const EINZAHL = {
|
||
aufgaben_ueberfaellig: (n) => `${n} ${n === 1 ? 'Aufgabe ist' : 'Aufgaben sind'} überfällig`,
|
||
aufgaben_heute: (n) => `${n} ${n === 1 ? 'Aufgabe ist' : 'Aufgaben sind'} heute fällig`,
|
||
aufgaben_review: (n) => `${n} ${n === 1 ? 'Aufgabe wartet' : 'Aufgaben warten'} auf Freigabe`,
|
||
termine_heute: (n) => `${n} ${n === 1 ? 'Termin steht' : 'Termine stehen'} heute an`,
|
||
protokoll_fehlt: (n) => `${n} ${n === 1 ? 'Gespräch hat' : 'Gespräche haben'} noch kein Protokoll`,
|
||
dateien_review: (n) => `${n} ${n === 1 ? 'Datei liegt' : 'Dateien liegen'} im Review`,
|
||
bereiche_dringend: (n) => `${n} ${n === 1 ? 'dringender Punkt' : 'dringende Punkte'} in den Bereichen`,
|
||
followup_faellig: (n) => `${n} Follow-up${n === 1 ? '' : 's'} ${n === 1 ? 'ist' : 'sind'} fällig`,
|
||
uebergabe_offen: (n) => `${n} übergebene${n === 1 ? 'r Lead wartet' : ' Leads warten'} auf das Onboarding`,
|
||
review_faellig: (n) => `${n} Review-Termin${n === 1 ? ' ist' : 'e sind'} überfällig`,
|
||
review_bald: (n) => `${n} Review${n === 1 ? ' steht' : 's stehen'} in den nächsten 7 Tagen an`,
|
||
ohne_betreuung: (n) => `${n} Creator ${n === 1 ? 'hat' : 'haben'} noch keine zuständige Person`,
|
||
ohne_profil: (n) => `${n} Creator-Profil${n === 1 ? ' ist' : 'e sind'} noch leer`,
|
||
ohne_startcheck: (n) => `${n} Creator ${n === 1 ? 'hat' : 'haben'} noch keinen Start-Check`,
|
||
startcheck_handlung: (n) => `${n} ${n === 1 ? 'Punkt im Start-Check braucht' : 'Punkte im Start-Check brauchen'} Handlung`,
|
||
};
|
||
|
||
async function hinweiseLaden() {
|
||
let daten;
|
||
try {
|
||
const antwort = await fetch('/workspace/api/hinweise', { credentials: 'same-origin' });
|
||
if (!antwort.ok) return;
|
||
daten = await antwort.json();
|
||
} catch { return; }
|
||
|
||
const liste = $('dran');
|
||
liste.textContent = '';
|
||
if (!daten.hinweise.length) { $('dran-block').hidden = true; return; }
|
||
|
||
for (const h of daten.hinweise) {
|
||
const li = document.createElement('li');
|
||
li.className = 'dran__punkt';
|
||
li.dataset.stufe = h.stufe;
|
||
|
||
/* Jeder Hinweis fuehrt irgendwohin -- ein Hinweis ohne Ziel ist
|
||
nur ein schlechtes Gewissen. */
|
||
const a = document.createElement('a');
|
||
a.className = 'dran__link';
|
||
a.href = h.ziel;
|
||
|
||
const text = document.createElement('span');
|
||
text.className = 'dran__text';
|
||
text.textContent = (EINZAHL[h.art] || ((n) => `${n} × ${h.text}`))(h.anzahl);
|
||
|
||
const pfeil = document.createElement('span');
|
||
pfeil.className = 'dran__pfeil';
|
||
pfeil.setAttribute('aria-hidden', 'true');
|
||
pfeil.textContent = '→';
|
||
|
||
a.append(text, pfeil);
|
||
li.append(a);
|
||
liste.append(li);
|
||
}
|
||
$('dran-block').hidden = false;
|
||
}
|
||
|
||
/* Die Zahlen kommen aus derselben Quelle wie das Brett und sind bereits
|
||
serverseitig auf die eigene Rolle eingegrenzt. */
|
||
const ZAHLEN = [
|
||
{ schluessel: 'ueberfaellig', name: 'Überfällig', warn: true },
|
||
{ schluessel: 'heute', name: 'Heute fällig' },
|
||
{ schluessel: 'offen', name: 'Offen' },
|
||
{ schluessel: 'arbeit', name: 'In Arbeit' },
|
||
{ schluessel: 'review', name: 'Review' },
|
||
{ schluessel: 'erledigt', name: 'Erledigt' },
|
||
];
|
||
|
||
async function zahlenLaden() {
|
||
let daten;
|
||
try {
|
||
const antwort = await fetch('/workspace/api/uebersicht', { credentials: 'same-origin' });
|
||
if (!antwort.ok) return;
|
||
daten = await antwort.json();
|
||
} catch { return; }
|
||
|
||
const liste = $('zahlen');
|
||
liste.textContent = '';
|
||
for (const z of ZAHLEN) {
|
||
const wert = daten[z.schluessel] ?? 0;
|
||
const li = document.createElement('li');
|
||
li.className = 'zahl';
|
||
if (z.warn && wert > 0) li.dataset.warn = 'ja';
|
||
|
||
const n = document.createElement('span');
|
||
n.className = 'zahl__wert';
|
||
n.textContent = String(wert);
|
||
|
||
const t = document.createElement('span');
|
||
t.className = 'zahl__name';
|
||
t.textContent = z.name;
|
||
|
||
li.append(n, t);
|
||
liste.append(li);
|
||
}
|
||
}
|
||
|
||
|
||
laden();
|
||
})();
|