Im Code stand die Begruendung selbst: "«Review» allein sagt einem Neuen nichts." Das Wort ist englisch, ein Hauptwort ohne Handlung, und es verraet nicht, WER jetzt dran ist. "Zur Freigabe" sagt beides: fertig von mir, wartet auf jemanden. Geaendert wurde nur, was ein Mensch LIEST -- der Zustandsschluessel bleibt `review`, der gehoert der Datenbank. Betroffen: Spalte und Weiterknopf im Aufgabenbrett, Dateien-Filter, Startseiten-Zaehler, Kachel-Unterzeile, Hinweis "Datei wartet auf Freigabe", Report. NICHT geaendert: der Kalender. Dort ist `review` eine TERMINART (ein Gespraech, in dem man zurueckschaut), kein Zustand. Ich hatte das beim Umbenennen selbst verwechselt und wieder zurueckgenommen -- ein Kommentar an der Stelle haelt die zwei Bedeutungen jetzt auseinander. pruef-sprung hing an der Wortwahl (`/review/i` auf der Beschriftung) und wurde rot, obwohl der Filter richtig stand. Sie prueft jetzt `data-status` -- den Schluessel, der sich nicht mit der Sprache aendert. DAZU, unabhaengig gefunden: Datumsfelder waren 48 px hoch, alle anderen Felder 44. Gemessen auf drei Seiten bei 390 px. Alle liegen auf dem 44-px-Beruehrziel -- nur das Datumsfeld drueckte sich darueber, weil Chromium in `::-webkit-datetime-edit` eine eigene Innenpolsterung setzt, die sogar ein gesetztes `height: 44px` ueberstimmt. Zwei Anteile, einzeln nachgemessen (jeder allein 48->46, erst beide zusammen 48->44): 1 px Polsterung oben und unten im Feldkasten, und eine Zeilenhoehe von 24 statt 22. Keine feste Hoehe gesetzt -- die Zeilenhoehe wird aus Beruehrziel und Polsterung gerechnet, damit sie mitwandert, wenn sich eines davon aendert. Geprueft: pruef-formulare 19/0 (war 16 mit 3 Fehlern), pruef-sprung 43/0, pruef-start-ansicht 151/0, pruef-aufgabenbrett 49/0, pruef-uebersicht 35/0, pruef-uebersicht-browser 20/0, pruef-deutsche-texte, pruef-css-klassen. Ausserdem: vorlagen.js geloescht (8,2 KB). `vorlagenBlock(` wurde inca104799eingebaut und in9267797dwieder ausgebaut -- seither laedt die Datei auf zwei Seiten, ohne dass jemand sie aufruft. Co-Authored-By: Claude Opus 5 <[email protected]>
529 lines
22 KiB
JavaScript
529 lines
22 KiB
JavaScript
/* ===================================================================
|
||
Dashboard -- die Gesamtuebersicht je Creator.
|
||
|
||
Das Konzept verspricht auf Seite 1 "UEBERSICHT -- alles zentral" und
|
||
auf Seite 2 "Die Admin-Rolle behaelt den Gesamtueberblick". Genau das
|
||
fehlte: Die Startseite ist ein Einstieg (was ist dran, wohin gehe
|
||
ich), aber sie beantwortet nicht "wie steht es um MEINE CREATOR".
|
||
|
||
Fuehrt keine eigenen Daten. Jede Zahl kommt aus Aufgaben, Terminen,
|
||
Bereichen, Profil und Start-Check -- und jede ist anklickbar, damit
|
||
man von der Zahl direkt zur Sache kommt. Eine Zahl ohne Weg dahin
|
||
waere nur ein schlechtes Gewissen.
|
||
=================================================================== */
|
||
(() => {
|
||
'use strict';
|
||
|
||
const $ = (id) => document.getElementById(id);
|
||
const el = (tag, klasse, text) => {
|
||
const k = document.createElement(tag);
|
||
if (klasse) k.className = klasse;
|
||
if (text !== undefined) k.textContent = text;
|
||
return k;
|
||
};
|
||
const melde = (t) => { $('fehler').textContent = t || ''; };
|
||
|
||
async function hole(pfad) {
|
||
const a = await fetch(pfad, { credentials: 'same-origin' });
|
||
if (a.status === 401) { location.assign('/workspace/'); throw new Error('abgemeldet'); }
|
||
return a;
|
||
}
|
||
|
||
const WOCHENTAG = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'];
|
||
|
||
const datum = (iso) => (iso ? iso.slice(0, 10).split('-').reverse().join('.') : '');
|
||
|
||
/* Ein Datum allein sagt wenig -- "Do, 04.09. · 18:00" ist die Angabe,
|
||
nach der man den Tag plant. */
|
||
function zeitpunkt(iso) {
|
||
if (!iso) return '';
|
||
const [d, z] = String(iso).split('T');
|
||
const [j, m, t] = d.split('-');
|
||
const tag = new Date(Number(j), Number(m) - 1, Number(t));
|
||
const w = Number.isNaN(tag.getTime()) ? '' : WOCHENTAG[tag.getDay()] + ', ';
|
||
return `${w}${t}.${m}.` + (z ? ' · ' + z.slice(0, 5) : '');
|
||
}
|
||
|
||
/* Wie lange ist die letzte Anmeldung her? Ab zwei Wochen wird die
|
||
Angabe gedaempft hervorgehoben -- das ist der Punkt, an dem jemand
|
||
abzudriften droht. */
|
||
function herLetzte(iso) {
|
||
if (!iso) return { text: 'noch nie angemeldet', kalt: true };
|
||
const tage = Math.floor((Date.now() - Date.parse(iso)) / 86400000);
|
||
if (!Number.isFinite(tage)) return { text: '' };
|
||
if (tage <= 0) return { text: 'heute da gewesen' };
|
||
if (tage === 1) return { text: 'gestern da gewesen' };
|
||
return { text: `seit ${tage} Tagen nicht da`, kalt: tage >= 14 };
|
||
}
|
||
|
||
/* JEDE SORTE HAT IHRE FARBE (screen16, 09.09.2026).
|
||
|
||
Filipe: "auch gerne mit mehreren farben arbeiten oder so damit die
|
||
wichtigsten sachen auch auffallen bitte."
|
||
|
||
Vorher trugen alle sechs Felder dasselbe Blau, und `data-warn`
|
||
faerbte zwei davon in DASSELBE Rot -- "ueberfaellig" und
|
||
"dringend" sahen identisch aus, obwohl das eine eine versaeumte
|
||
Frist ist und das andere eine Sache, die schnell muss. Zwei
|
||
verschiedene Alarme, die gleich aussehen, sind ein Alarm.
|
||
|
||
`sorte` ist ein SCHLUESSEL, kein Text: Die Beschriftung heisst
|
||
"erledigt 30 T." und koennte sich morgen aendern; die Farbe soll
|
||
das nicht mitmachen. Welcher Ton wohin gehoert, steht in
|
||
uebersicht.css an einer Stelle.
|
||
|
||
DIE FARBE IST NIE DER EINZIGE TRAEGER -- die Beschriftung steht
|
||
darunter, in Grossbuchstaben und ausgeschrieben. Wer Farben
|
||
schlecht unterscheidet, liest dasselbe wie alle anderen. */
|
||
function wert(anzahl, name, ziel, warn, sorte) {
|
||
const a = el(ziel ? 'a' : 'span', 'wert');
|
||
if (ziel) a.href = ziel;
|
||
if (sorte) a.dataset.sorte = sorte;
|
||
if (warn && anzahl > 0) a.dataset.warn = 'ja';
|
||
if (!anzahl) a.dataset.leer = 'ja';
|
||
a.append(el('span', 'wert__zahl', String(anzahl)));
|
||
a.append(el('span', 'wert__name', name));
|
||
return a;
|
||
}
|
||
|
||
/* ---------- Die Zahlen in der Karte -------------------------------------
|
||
|
||
Bis zum 06.09.2026 zeigte diese Karte ausschliesslich ARBEIT:
|
||
überfällig, offen, in Arbeit, im Review. Über das GESCHÄFT stand
|
||
hier nichts -- und damit fehlte ausgerechnet die Zeile, wegen der
|
||
man ein Dashboard aufmacht. Ein Creator konnte null überfällige
|
||
Aufgaben haben und gleichzeitig seit drei Wochen einbrechen; die
|
||
Karte sah dann tadellos aus.
|
||
|
||
Drei Zahlen, nicht sieben: Diamanten (das Ergebnis), LIVE-Tage (die
|
||
Anwesenheit) und die Verweildauer (die Leitgrösse, an der der
|
||
Algorithmus alles aufhängt). Alles Weitere steht eine Seite weiter.
|
||
Eine Karte, die alles zeigt, zeigt nichts. */
|
||
const zahlDe = (n) => Number(n || 0).toLocaleString('de-DE');
|
||
|
||
function dauerKurz(s) {
|
||
if (s === null || s === undefined) return '–';
|
||
const m = Math.floor(s / 60);
|
||
return m ? `${m}:${String(Math.round(s % 60)).padStart(2, '0')}` : `${Math.round(s)}s`;
|
||
}
|
||
|
||
function zahlenZeile(z, creatorId) {
|
||
const a = el('a', 'kzahlen');
|
||
a.href = `leistung.html?creator=${creatorId}`;
|
||
|
||
/* KEINE ZAHLEN IST EINE EIGENE AUSSAGE, keine Null. Wer hier "0
|
||
Diamanten" schreibt, behauptet eine Messung, die es nicht gab --
|
||
und der Betreuer sucht danach den Einbruch statt den fehlenden
|
||
Export. */
|
||
if (!z || !z.tage_erfasst) {
|
||
a.dataset.leer = 'ja';
|
||
a.append(el('span', 'kzahlen__leer', 'Für diese Woche sind noch keine Zahlen erfasst'));
|
||
return a;
|
||
}
|
||
|
||
const feld = (name, text, extra) => {
|
||
const s = el('span', 'kzahlen__feld');
|
||
s.append(el('span', 'kzahlen__name', name));
|
||
s.append(el('span', 'kzahlen__wert', text));
|
||
if (extra) s.append(extra);
|
||
return s;
|
||
};
|
||
|
||
/* Der Vergleich zur Vorwoche steht DIREKT an der Zahl. Ohne ihn ist
|
||
"1.240" eine Behauptung; mit ihm ist es eine Auskunft. */
|
||
let wandel = null;
|
||
if (z.wandel_diamanten?.prozent !== undefined) {
|
||
const p = z.wandel_diamanten.prozent;
|
||
wandel = el('span', 'kzahlen__wandel', `${p > 0 ? '+' : ''}${p} %`);
|
||
wandel.dataset.richtung = p > 0 ? 'hoch' : p < 0 ? 'runter' : 'gleich';
|
||
}
|
||
|
||
a.append(feld('Diamanten', zahlDe(z.diamanten), wandel));
|
||
a.append(feld('LIVE-Tage', `${z.gueltige_tage} / 7`));
|
||
a.append(feld('Ø Verweildauer', dauerKurz(z.verweildauer_s)));
|
||
return a;
|
||
}
|
||
|
||
function karte(c, checkGesamt, eigen, zahlen) {
|
||
const k = el('article', 'creator');
|
||
if (c.ueberfaellig > 0 || c.dringend > 0) k.dataset.achtung = 'ja';
|
||
|
||
const kopf = el('div', 'creator__kopf');
|
||
|
||
const links = el('div', 'creator__wer');
|
||
links.append(el('h2', 'creator__name', c.name));
|
||
const unter = el('p', 'creator__unter');
|
||
const her = herLetzte(c.letzter_login);
|
||
const teile = [];
|
||
if (!eigen && c.betreuer) teile.push('betreut von ' + c.betreuer);
|
||
if (!eigen && !c.betreuer) teile.push('noch niemand zuständig');
|
||
if (her.text) teile.push(her.text);
|
||
unter.textContent = teile.join(' · ');
|
||
if (her.kalt || (!eigen && !c.betreuer)) unter.dataset.kalt = 'ja';
|
||
links.append(unter);
|
||
kopf.append(links);
|
||
|
||
/* Die einzige Angabe, die nach vorn zeigt -- deshalb steht sie
|
||
abgesetzt oben rechts und nicht zwischen den Zaehlungen. */
|
||
const rechts = el('div', 'naechstes');
|
||
if (c.naechster_termin) {
|
||
rechts.append(el('span', 'naechstes__schild', 'Nächster Termin'));
|
||
const a = el('a', 'naechstes__wert', zeitpunkt(c.naechster_termin));
|
||
a.href = 'kalender.html';
|
||
rechts.append(a);
|
||
} else if (c.naechster_review) {
|
||
/* "Nächster Review" war grammatisch schief und sagte nicht, dass
|
||
ein DATUM folgt. Kuerzer als das Geschwisterschild daneben,
|
||
kann also nicht umbrechen, wo jenes passt. */
|
||
rechts.append(el('span', 'naechstes__schild', 'Review-Termin'));
|
||
const a = el('a', 'naechstes__wert', datum(c.naechster_review));
|
||
a.href = 'report.html';
|
||
rechts.append(a);
|
||
} else {
|
||
rechts.append(el('span', 'naechstes__schild', 'Nächster Termin'));
|
||
rechts.append(el('span', 'naechstes__wert naechstes__wert--leer', 'keiner geplant'));
|
||
}
|
||
kopf.append(rechts);
|
||
k.append(kopf);
|
||
|
||
const werte = el('div', 'werte');
|
||
werte.append(wert(c.ueberfaellig, 'überfällig', 'aufgaben.html', true, 'ueberfaellig'));
|
||
werte.append(wert(c.dringend, 'dringend', 'bereich.html?b=live', true, 'dringend'));
|
||
werte.append(wert(c.offen, 'offen', 'aufgaben.html', false, 'offen'));
|
||
werte.append(wert(c.arbeit, 'in Arbeit', 'aufgaben.html', false, 'arbeit'));
|
||
werte.append(wert(c.review, 'im Review', 'aufgaben.html', false, 'review'));
|
||
werte.append(wert(c.erledigt30, 'erledigt 30 T.', 'aufgaben.html', false, 'erledigt'));
|
||
k.append(werte);
|
||
|
||
k.append(zahlenZeile(zahlen, c.id));
|
||
|
||
/* Start-Check als Balken statt als Zahl -- "4 von 16" liest man,
|
||
ein Viertel gefuellt sieht man. */
|
||
const check = el('a', 'fortschritt');
|
||
check.href = 'startcheck.html';
|
||
const geprueft = c.check_geprueft || 0;
|
||
const anteil = checkGesamt ? Math.round((geprueft / checkGesamt) * 100) : 0;
|
||
if (geprueft >= checkGesamt && checkGesamt) check.dataset.fertig = 'ja';
|
||
|
||
const zeile = el('p', 'fortschritt__zeile');
|
||
zeile.append(el('span', 'fortschritt__schild', 'Start-Check'));
|
||
zeile.append(el('span', 'fortschritt__text',
|
||
geprueft ? `${geprueft} von ${checkGesamt} geprüft` : 'noch nicht begonnen'));
|
||
if (c.check_handlung > 0) {
|
||
zeile.append(el('span', 'fortschritt__warn',
|
||
`${c.check_handlung} × Handlungsbedarf`));
|
||
}
|
||
check.append(zeile);
|
||
|
||
const balken = el('span', 'fortschritt__balken');
|
||
const fuellung = el('span', 'fortschritt__fuellung');
|
||
fuellung.style.width = anteil + '%';
|
||
if (c.check_handlung > 0) fuellung.dataset.warn = 'ja';
|
||
balken.append(fuellung);
|
||
check.append(balken);
|
||
k.append(check);
|
||
|
||
if (!c.hat_profil) {
|
||
const a = el('a', 'creator__luecke', eigen
|
||
? 'Dein Profil ist noch leer – jetzt ausfüllen'
|
||
: 'Profil noch nicht ausgefüllt');
|
||
a.href = 'profil.html';
|
||
k.append(a);
|
||
}
|
||
return k;
|
||
}
|
||
|
||
function summenZeigen(liste) {
|
||
const s = $('summen');
|
||
s.textContent = '';
|
||
if (liste.length < 2) { s.hidden = true; return; }
|
||
const summe = (feld) => liste.reduce((n, c) => n + (c[feld] || 0), 0);
|
||
const felder = [
|
||
[liste.length, 'Creator', false],
|
||
[summe('ueberfaellig'), 'überfällig', true],
|
||
[summe('dringend'), 'dringende Punkte', true],
|
||
[summe('offen') + summe('arbeit'), 'offen & in Arbeit', false],
|
||
[summe('review'), 'im Review', false],
|
||
[summe('erledigt30'), 'erledigt · 30 Tage', false],
|
||
];
|
||
for (const [zahl, name, warn] of felder) {
|
||
const k = el('div', 'summe');
|
||
if (warn && zahl > 0) k.dataset.warn = 'ja';
|
||
k.append(el('span', 'summe__zahl', String(zahl)));
|
||
k.append(el('span', 'summe__name', name));
|
||
s.append(k);
|
||
}
|
||
s.hidden = false;
|
||
}
|
||
|
||
/* ---------- Die Frühwarnung ---------------------------------------------
|
||
|
||
Sechs benannte Signale, jedes im Klartext und mit Beleg. Bewusst
|
||
keine Punktzahl: "Abwanderungsrisiko 73" klingt nach Wissenschaft,
|
||
ist aber eine Behauptung, der man nicht widersprechen kann. Sechs
|
||
Sätze, die man einzeln bestreiten kann, sind ehrlicher -- und
|
||
handlungsfähiger, denn jeder sagt schon, was zu tun wäre. */
|
||
async function fruehwarnungLaden() {
|
||
let d;
|
||
try {
|
||
const a = await hole('/workspace/api/fruehwarnung');
|
||
if (!a.ok) return; /* still bleiben, nicht stören */
|
||
d = await a.json();
|
||
} catch { return; }
|
||
|
||
const leute = d.creator || [];
|
||
if (!leute.length) return; /* nichts zu melden ist kein Block */
|
||
|
||
const ziel = $('warnliste');
|
||
ziel.textContent = '';
|
||
|
||
for (const c of leute) {
|
||
const k = el('article', 'warnkarte');
|
||
|
||
const kopf = el('div', 'warnkarte__kopf');
|
||
const name = el('a', 'warnkarte__name', c.name);
|
||
name.href = `leistung.html?creator=${c.id}`;
|
||
kopf.append(name);
|
||
/* Der Weg zum Gespräch steht gleich daneben. Bei vier der sechs
|
||
Signale ist "einmal schreiben" die richtige nächste Handlung --
|
||
und ein Hinweis ohne Weg dahin ist nur ein schlechtes
|
||
Gewissen. */
|
||
const schreiben = el('a', 'warnkarte__weg', 'Schreiben');
|
||
schreiben.href = `chat.html?mit=${c.id}`;
|
||
kopf.append(schreiben);
|
||
k.append(kopf);
|
||
|
||
const liste = el('ul', 'warnkarte__signale');
|
||
for (const s of c.signale || []) {
|
||
const li = el('li', 'warnsignal');
|
||
li.dataset.art = s.art;
|
||
li.append(el('span', 'warnsignal__text', s.text));
|
||
if (s.beleg) li.append(el('span', 'warnsignal__beleg', s.beleg));
|
||
liste.append(li);
|
||
}
|
||
k.append(liste);
|
||
ziel.append(k);
|
||
}
|
||
$('fruehwarnung').hidden = false;
|
||
}
|
||
|
||
/* ===================================================================
|
||
CREATOR ANLEGEN (07.09.2026)
|
||
|
||
Wunsch Filipe: "manager sollen ab jetzt creator und auch wirklich
|
||
nur creator hinzufuegen koennen".
|
||
|
||
Der Weg dahin ist ein EIGENER (POST /workspace/api/creator-anlegen)
|
||
und nicht die Personenverwaltung -- die bleibt DogFather
|
||
vorbehalten, dort liegen Rollen, Codes, Sperren und das Protokoll.
|
||
Dieser Weg kann nichts anderes, als einen Creator anzulegen; die
|
||
Rolle steht nicht einmal im Aufruf.
|
||
|
||
DER KNOPF WIRD VERSTECKT, NICHT DIE ABSAGE ABGEFANGEN. Beides
|
||
zusammen: Der Server laesst Scouts und Creator ohnehin nicht durch
|
||
(404). Das Verstecken erspart ihnen nur einen Knopf, der eine
|
||
Absage bringt -- es IST nicht die Sicherung.
|
||
|
||
DER CODE WIRD EINMAL GEZEIGT UND NIE NACHGELADEN. Er steht nur in
|
||
der Antwort auf das Anlegen; danach gibt es ihn nirgends mehr.
|
||
Deshalb bleibt der Dialog nach dem Anlegen OFFEN und schliesst sich
|
||
nicht von selbst -- ein Fenster, das sich mit dem einzigen
|
||
Zugangscode schliesst, bevor jemand ihn kopiert hat, ist ein
|
||
Datenverlust mit Ansage. */
|
||
function creatorAnlegenBereit(ich) {
|
||
const knopf = document.getElementById('creator-neu');
|
||
const dialog = document.getElementById('creator-dialog');
|
||
if (!knopf || !dialog || !ich) return;
|
||
/* Leitung -- aus der einen Rollenquelle, nicht abgeschrieben.
|
||
Abgeschrieben stand hier ['admin','manager'], also OHNE Spicy
|
||
Media: Der Server haette sie anlegen lassen (nurLeitung schliesst
|
||
sie ein), aber den Knopf dazu hat sie nie gesehen. Ein fehlender
|
||
Knopf sieht nicht nach Fehler aus, sondern nach Absicht. */
|
||
if (!window.Bereiche.LEITUNG.has(ich.rolle)) return;
|
||
knopf.hidden = false;
|
||
|
||
const f = (id) => document.getElementById(id);
|
||
const zuruecksetzen = () => {
|
||
f('creator-form').reset();
|
||
f('cn-code-zeile').hidden = true;
|
||
f('cn-fehler').textContent = '';
|
||
f('cn-speichern').hidden = false;
|
||
f('cn-zu').textContent = 'Abbrechen';
|
||
};
|
||
|
||
knopf.addEventListener('click', async () => {
|
||
zuruecksetzen();
|
||
/* Die Scoutliste kommt vom Server -- und zwar dieselbe, die er
|
||
beim Anlegen auch annimmt. Eine im Browser zusammengestellte
|
||
Liste waere die zweite Wahrheit, und die beiden laufen
|
||
auseinander. */
|
||
try {
|
||
const a = await hole('/workspace/api/creator-anlegen/scouts');
|
||
const scouts = a.ok ? (await a.json()).scouts || [] : [];
|
||
const w = f('cn-scout');
|
||
w.textContent = '';
|
||
const eigen = document.createElement('option');
|
||
eigen.value = ''; eigen.textContent = `${ich.name} (du)`;
|
||
w.append(eigen);
|
||
for (const sc of scouts) {
|
||
const o = document.createElement('option');
|
||
o.value = String(sc.id); o.textContent = sc.name;
|
||
w.append(o);
|
||
}
|
||
/* Gibt es keinen Scout, gibt es auch nichts zu waehlen -- ein
|
||
Auswahlfeld mit genau einem Eintrag ist eine Frage ohne
|
||
Antwortmoeglichkeit. */
|
||
f('cn-scout-zeile').hidden = scouts.length === 0;
|
||
} catch { f('cn-scout-zeile').hidden = true; }
|
||
dialog.showModal();
|
||
f('cn-name').focus();
|
||
});
|
||
|
||
f('cn-zu').addEventListener('click', () => dialog.close());
|
||
|
||
f('creator-form').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const name = f('cn-name').value.trim();
|
||
if (name.length < 2) { f('cn-fehler').textContent = 'Der Name fehlt.'; return; }
|
||
const knopfS = f('cn-speichern');
|
||
knopfS.disabled = true;
|
||
f('cn-fehler').textContent = '';
|
||
try {
|
||
const a = await hole('/workspace/api/creator-anlegen', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ name, betreuer_id: f('cn-scout').value || null }),
|
||
});
|
||
const antwort = await a.json().catch(() => ({}));
|
||
if (!a.ok) {
|
||
f('cn-fehler').textContent = sagWas(antwort.fehler, 'Ging nicht.');
|
||
return;
|
||
}
|
||
f('cn-code-name').textContent = antwort.name;
|
||
f('cn-code').textContent = antwort.code;
|
||
f('cn-code-zeile').hidden = false;
|
||
knopfS.hidden = true;
|
||
f('cn-zu').textContent = 'Fertig';
|
||
/* Die Liste dahinter wird neu geladen, sobald der Dialog zugeht
|
||
-- der neue Creator soll da stehen, wo man ihn erwartet. */
|
||
dialog.addEventListener('close', () => location.reload(), { once: true });
|
||
} catch {
|
||
f('cn-fehler').textContent = 'Ging nicht.';
|
||
} finally { knopfS.disabled = false; }
|
||
});
|
||
}
|
||
|
||
(async () => {
|
||
let ich;
|
||
try {
|
||
const a = await hole('/workspace/api/ich');
|
||
if (!a.ok) { location.assign('/workspace/'); return; }
|
||
ich = await a.json();
|
||
} catch {
|
||
/* NICHT MEHR STUMM (20.09.2026). Hier stand `catch { return; }`
|
||
-- und damit blieb die Seite bei jedem Netzaussetzer fuer
|
||
immer auf "wird geladen …" stehen. Gemessen auf 13 von 21
|
||
Seiten. `ladefehler()` sagt, was los ist, und bietet einen
|
||
Knopf an, der es noch einmal versucht. */
|
||
window.ladefehler?.();
|
||
return;
|
||
}
|
||
window.werZeigen?.(ich);
|
||
creatorAnlegenBereit(ich);
|
||
|
||
let d;
|
||
try {
|
||
const a = await hole('/workspace/api/uebersicht/creator');
|
||
if (!a.ok) { melde('Die Übersicht konnte gerade nicht geladen werden.'); return; }
|
||
d = await a.json();
|
||
} catch { return; }
|
||
|
||
const ziel = $('liste');
|
||
ziel.textContent = '';
|
||
ziel.setAttribute('aria-busy', 'false');
|
||
|
||
const creator = d.creator || [];
|
||
const eigen = !!d.eigen;
|
||
|
||
if (eigen) {
|
||
$('titel').textContent = 'Mein Stand';
|
||
$('unterzeile').textContent = 'Wo du gerade stehst – aus deinen Aufgaben, Terminen und dem Start-Check.';
|
||
} else if (!creator.length) {
|
||
/* Ein Manager sah hier bis zum 03.09.2026 ALLE Creator und
|
||
landete deshalb nie in diesem Zweig. Seit die Zuteilung zählt,
|
||
steht er ohne zugeteilte Creator vor einer leeren Seite -- und
|
||
der Satz für den Scout ("DogFather legt fest, wen du betreust")
|
||
passte für ihn nur zur Hälfte: Er bekommt seine Creator auch
|
||
über seine Scouts.
|
||
|
||
Ein leerer Bereich ohne Erklärung sieht aus wie ein Fehler.
|
||
Deshalb steht hier für jede Rolle der Satz, der wirklich
|
||
zutrifft. */
|
||
const satz = ich.rolle === 'scout'
|
||
? 'Dir ist noch kein Creator zugeteilt.'
|
||
: ich.rolle === 'manager'
|
||
? 'Dir ist noch kein Creator zugeteilt – weder direkt noch über einen deiner Scouts.'
|
||
: 'Es gibt noch keinen Creator.';
|
||
$('unterzeile').textContent = satz;
|
||
ziel.append(el('p', 'leer-hinweis', satz + ' '
|
||
+ (ich.rolle === 'admin'
|
||
? 'Lege unter „Personen & Zugänge“ den ersten Creator an.'
|
||
: 'DogFather legt unter „Personen & Zugänge“ fest, wen du betreust.')));
|
||
return;
|
||
} else {
|
||
$('unterzeile').textContent = creator.length === 1
|
||
? 'Ein Creator – mit einem Blick sehen, wo es hakt und wo es läuft.'
|
||
: `${creator.length} Creator – mit einem Blick sehen, wo es hakt und wo es läuft.`;
|
||
}
|
||
|
||
summenZeigen(creator);
|
||
|
||
/* Die Zahlen kommen aus EINER zusätzlichen Abfrage für alle Creator,
|
||
nicht aus einer je Karte. Bei zwölf Creators wären das zwölf
|
||
Anfragen, die alle dasselbe tun -- und die Seite stünde so lange
|
||
halb leer.
|
||
|
||
Und sie darf nicht scheitern dürfen: Wenn das Zahlen-Modul
|
||
stolpert, bleibt die Übersicht wie vorher stehen, nur ohne die
|
||
drei Zeilen. Eine leere Seite wäre der schlechtere Ausgang. */
|
||
let zahlenNach = new Map();
|
||
try {
|
||
const a = await hole('/workspace/api/leistung');
|
||
if (a.ok) {
|
||
const z = await a.json();
|
||
zahlenNach = new Map((z.creator || []).map((c) => [c.id, c]));
|
||
}
|
||
} catch { /* die Karten stehen auch ohne */ }
|
||
|
||
/* Wo etwas brennt, steht oben. Sortiert wird nach dem, was Handlung
|
||
verlangt -- nicht alphabetisch. Bei Gleichstand dann doch nach
|
||
Namen, damit die Reihenfolge stabil bleibt. */
|
||
const sortiert = creator.slice().sort((a, b) =>
|
||
(b.ueberfaellig + b.dringend) - (a.ueberfaellig + a.dringend)
|
||
|| String(a.name).localeCompare(String(b.name), 'de'));
|
||
|
||
const box = el('div', 'creator-liste');
|
||
for (const c of sortiert) {
|
||
box.append(karte(c, d.check_gesamt || 16, eigen, zahlenNach.get(c.id)));
|
||
}
|
||
ziel.append(box);
|
||
|
||
/* Zum Schluss, und ohne await davor: Die Karten sollen nicht auf die
|
||
Frühwarnung warten. Sie schiebt sich oben ein, wenn sie da ist --
|
||
für einen Creator kommt sie nie (der Server liefert ihm nichts). */
|
||
if (!eigen) fruehwarnungLaden();
|
||
|
||
if (d.stand) {
|
||
/* Der Server schickt UTC. zeitpunkt() darf hier NICHT genommen
|
||
werden -- das schneidet die Zeichenkette nur auf und zeigte im
|
||
Sommer zwei Stunden zu wenig an. */
|
||
const t = new Date(d.stand);
|
||
const p = (n) => String(n).padStart(2, '0');
|
||
const s = $('stand');
|
||
s.textContent = `Stand: ${p(t.getHours())}:${p(t.getMinutes())} Uhr · `
|
||
+ 'wird bei jedem Aufruf neu ermittelt';
|
||
s.hidden = false;
|
||
}
|
||
})();
|
||
})();
|