Aufbau steht (Ring links, Titel mittig, Uhr rechts), aber pruef-start-ansicht meldet 3 Befunde am MAUS-LICHT der Kacheln. Gemessen: alter Stand 0 Befunde, dieser Stand 3 — kommt also von hier. Kein JS-Fehler (pageerror/console sind still), also Zeitverhalten: Die zusaetzliche Abfrage /api/zentrale verzoegert vermutlich den Aufbau der Kacheln ueber den Zeitpunkt hinaus, an dem kopf.js lichtFolgen ruft. Ausserdem offen: 2 Schriftgroessen unter 11,5 px (pruef-css-klassen). NICHT ausliefern.
1145 lines
50 KiB
JavaScript
1145 lines
50 KiB
JavaScript
/* ===================================================================
|
||
Startseite nach dem Anmelden.
|
||
|
||
Sie war eine Wand aus siebzehn gleichen Rechtecken, auf denen jeweils
|
||
"ÖFFNEN" stand. Wenn alles gleich aussieht, ist alles gleich wichtig
|
||
-- also nichts. Und "ÖFFNEN" ist keine Information, das weiss man von
|
||
einer Kachel ohnehin.
|
||
|
||
Drei Dinge machen daraus etwas Brauchbares:
|
||
|
||
1. GRUPPEN. Was man täglich braucht, was zu einem Creator gehört, was
|
||
den Laden am Laufen hält -- das sind drei verschiedene Absichten
|
||
und nicht eine Liste.
|
||
|
||
2. ZAHLEN STATT "ÖFFNEN". Auf der Kachel steht, wo Arbeit liegt.
|
||
Dieselben Quellen wie die Hinweise darüber, kein zweiter Zähler.
|
||
Eine Kachel, die "3" sagt, ist zehnmal mehr wert als eine, die
|
||
"öffnen" sagt.
|
||
|
||
3. ZEICHEN UND FARBE. Ein eigenes Zeichen je Bereich und ein eigener
|
||
Farbton -- man findet sie nach kurzer Zeit blind.
|
||
|
||
Die Seite ist bereits serverseitig geschützt (workspace.js). Dieses
|
||
Skript holt nur noch, WER angemeldet ist.
|
||
=================================================================== */
|
||
(() => {
|
||
'use strict';
|
||
|
||
const ROLLENTEXT = {
|
||
spicy: 'Spicy Media · Überblick über alle, ohne Automationen',
|
||
admin: 'DogFather · Gesamtübersicht und Freigaben',
|
||
/* Der Satz stimmte bis zum 07.09.2026. Seither legen Manager
|
||
Creator an -- und ein Rollentext, der etwas anderes verspricht als
|
||
die Oberflaeche kann, ist schlimmer als gar keiner. */
|
||
manager: 'Manager · dieselben Rechte, ohne Personenverwaltung – legt Creator an',
|
||
creator: 'Creator · dein eigener Bereich',
|
||
scout: 'Scout · deine Pipeline und die Creator, die du betreust',
|
||
};
|
||
|
||
/* Zeichen und Bereiche kommen aus assets/js/bereiche.js -- derselben
|
||
Quelle, aus der auch die Kopfleisten der Unterseiten ihr Zeichen und
|
||
ihre Farbe nehmen. Zwei Listen wären der sichere Weg dazu, dass ein
|
||
Bereich hier gelb und dort grün ist. */
|
||
const { ZEICHEN, GRUPPEN, zeichenBauen } = window.Bereiche;
|
||
|
||
|
||
/* Welcher Hinweis gehört auf welche Kachel. Die Zahl auf der Kachel ist
|
||
damit KEIN neuer Zähler, sondern dieselbe Quelle wie die Liste
|
||
darüber -- sonst stünden irgendwann zwei verschiedene Wahrheiten
|
||
übereinander auf einem Bildschirm. */
|
||
const HINWEIS_ZU_KACHEL = {
|
||
aufgaben_ueberfaellig: 'Aufgaben',
|
||
aufgaben_heute: 'Aufgaben',
|
||
aufgaben_review: 'Aufgaben',
|
||
termine_heute: 'Kalender',
|
||
protokoll_fehlt: 'Calls & Protokolle',
|
||
dateien_review: 'Dateien',
|
||
bereiche_dringend: 'LIVE-Analyse',
|
||
startcheck_handlung: 'Start-Check',
|
||
ohne_startcheck: 'Start-Check',
|
||
followup_faellig: 'Scout-Pipeline',
|
||
uebergabe_offen: 'Scout-Pipeline',
|
||
review_faellig: 'Reports & Review',
|
||
review_bald: 'Reports & Review',
|
||
ohne_betreuung: 'Personen & Zugänge',
|
||
ohne_profil: 'Creator-Profile',
|
||
markiert_live: 'LIVE-Analyse',
|
||
markiert_content: 'Content-Ideen',
|
||
markiert_community: 'Community',
|
||
markiert_technik: 'Technik',
|
||
};
|
||
|
||
/* Diese Hinweise bekommen zusätzlich das Merkzeichen auf die Kachel.
|
||
Sie sind etwas anderes als "3 Aufgaben offen": Da hat jemand
|
||
persönlich etwas markiert und wartet auf eine Reaktion. Eine Zahl
|
||
allein macht diesen Unterschied nicht sichtbar. */
|
||
const MIT_MERKZEICHEN = new Set([
|
||
'markiert_live', 'markiert_content', 'markiert_community', 'markiert_technik',
|
||
]);
|
||
|
||
/* Findet einen Bereich über seinen Namen -- damit Hinweis und Kachel
|
||
dasselbe Zeichen und denselben Farbton benutzen, ohne dass die
|
||
Zuordnung ein zweites Mal irgendwo stünde. */
|
||
const bereichZu = (name) => {
|
||
if (!name) return null;
|
||
for (const g of GRUPPEN) {
|
||
const b = g.bereiche.find((x) => x.name === name);
|
||
if (b) return b;
|
||
}
|
||
return null;
|
||
};
|
||
|
||
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;
|
||
};
|
||
|
||
let ich = null;
|
||
/* WESSEN ARBEITSPLATZ WIRD GEZEIGT (02.09.2026).
|
||
|
||
`ich` ist, WER ANGEMELDET ist -- das bleibt so und gilt fuer alles,
|
||
was schreibt oder Rechte hat. `zeigt` ist, WESSEN Arbeitsplatz auf
|
||
dem Bildschirm steht. Im Normalfall dasselbe.
|
||
|
||
Vorher gab es nur `ich`, und die Folge war ein halb gebauter
|
||
Umschalter: Die Listen kamen bereits von der gewaehlten Person,
|
||
Kacheln, Rolle und Begruessung aber weiterhin von DogFather. Er sah
|
||
einen Creator-Arbeitsplatz in seiner eigenen Verkleidung -- gemeldet
|
||
mit "ich seh immer noch die Seite genau wie meine". */
|
||
let zeigt = null;
|
||
let hinweise = [];
|
||
|
||
/* ---------- Aufbau ------------------------------------------------------- */
|
||
|
||
function bereicheBauen() {
|
||
const ziel = $('bereiche');
|
||
ziel.textContent = '';
|
||
|
||
/* Zahlen je Kachel aus den Hinweisen zusammenzählen. Warnstufe
|
||
schlägt durch: Eine Kachel mit etwas Überfälligem ist rot, auch
|
||
wenn daneben Harmloses mitgezählt wird. */
|
||
const zahlen = new Map();
|
||
for (const h of hinweise) {
|
||
const kachel = HINWEIS_ZU_KACHEL[h.art];
|
||
if (!kachel) continue;
|
||
const bisher = zahlen.get(kachel) || { n: 0, warn: false, merk: 0 };
|
||
bisher.n += h.anzahl;
|
||
if (h.stufe === 'warnung') bisher.warn = true;
|
||
if (MIT_MERKZEICHEN.has(h.art)) bisher.merk += h.anzahl;
|
||
zahlen.set(kachel, bisher);
|
||
}
|
||
|
||
let laufNr = 0;
|
||
for (const gruppe of GRUPPEN) {
|
||
/* Nach der ANGESEHENEN Rolle -- sonst stuenden bei der Sicht auf
|
||
einen Creator weiterhin "Personen & Zugaenge" und "Automationen"
|
||
da, die er gar nicht hat. Genau daran war zu sehen, dass die
|
||
Sicht nicht greift. */
|
||
const meine = gruppe.bereiche.filter((b) => b.rollen.includes(zeigt.rolle));
|
||
if (!meine.length) continue;
|
||
|
||
const abschnitt = el('section', 'gruppe');
|
||
/* Manche Gruppen heißen für einen Creator anders -- siehe GRUPPEN. */
|
||
const istCreator = zeigt.rolle === 'creator';
|
||
|
||
/* ===============================================================
|
||
JEDE GRUPPE LAESST SICH ZUKLAPPEN (07.09.2026)
|
||
|
||
Wunsch Filipe: "diese kategorien soll man auch zu und aufmachen
|
||
koennen so wie es einem lieb ist gerade."
|
||
|
||
DER KOPF IST DER SCHALTER, nicht ein Knopf daneben. Eine
|
||
Ueberschrift, neben der ein kleines Dreieck sitzt, laedt zum
|
||
Zielen ein; eine Ueberschrift, die SELBST der Schalter ist,
|
||
trifft man immer. Deshalb <button> statt <div> -- damit kommt
|
||
die Tastaturbedienung und die Ansage fuer Vorleseprogramme
|
||
geschenkt, statt sie mit tabindex und role nachzubauen.
|
||
|
||
DER ZUSTAND WIRD GEMERKT. "So wie es einem lieb ist" heisst:
|
||
Er muss den naechsten Aufruf ueberleben. Gespeichert wird je
|
||
Gruppe und je ANGESEHENER Rolle -- DogFather, der sich einen
|
||
Creator ansieht, hat dort eine andere Aufteilung im Kopf als
|
||
auf seiner eigenen Seite.
|
||
|
||
Faellt localStorage aus (privates Fenster, volle Ablage), ist
|
||
das kein Fehler: Dann steht eben alles offen. Ein Merker, der
|
||
die Seite mitreisst, waere schlimmer als ein vergessener. */
|
||
const kopf = el('button', 'gruppe__kopf');
|
||
kopf.type = 'button';
|
||
const schluessel = `gruppe:${zeigt.rolle}:${gruppe.name}`;
|
||
let zu = false;
|
||
try { zu = localStorage.getItem(schluessel) === 'zu'; } catch { zu = false; }
|
||
|
||
kopf.append(el('h2', 'gruppe__name',
|
||
(istCreator && gruppe.nameCreator) || gruppe.name));
|
||
kopf.append(el('span', 'gruppe__unter',
|
||
(istCreator && gruppe.unterCreator) || gruppe.unter));
|
||
/* Die Zahl sagt, was hinter einer zugeklappten Gruppe steckt --
|
||
ohne sie klappt man drei Gruppen auf, um zu suchen. */
|
||
kopf.append(el('span', 'gruppe__zahl', String(meine.length)));
|
||
const pfeil = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||
pfeil.setAttribute('class', 'gruppe__pfeil');
|
||
pfeil.setAttribute('viewBox', '0 0 24 24');
|
||
pfeil.setAttribute('aria-hidden', 'true');
|
||
pfeil.innerHTML = '<path d="m6 9.5 6 6 6-6" fill="none" stroke="currentColor"'
|
||
+ ' stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>';
|
||
kopf.append(pfeil);
|
||
abschnitt.append(kopf);
|
||
|
||
const liste = el('ul', 'kacheln');
|
||
for (const b of meine) {
|
||
const li = el('li', 'kachel');
|
||
li.dataset.ton = String(b.ton);
|
||
if (b.gross) li.dataset.gross = 'ja';
|
||
/* Der Einlauf wird gestaffelt -- aber über eine Eigenschaft, die
|
||
bei "weniger Bewegung" nichts tut (siehe start.css). */
|
||
li.style.setProperty('--nr', String(laufNr++));
|
||
|
||
const a = el('a', 'kachel__link');
|
||
a.href = b.ziel;
|
||
|
||
/* Das Wasserzeichen: dasselbe Zeichen noch einmal, riesig und
|
||
fast unsichtbar in der Ecke. Es macht jede Kachel auf den
|
||
ersten Blick unverwechselbar, ohne dass man es bewusst
|
||
wahrnimmt -- und es ist der Grund, warum siebzehn Kacheln
|
||
nicht mehr wie siebzehn Kästen aussehen.
|
||
|
||
Steht als Erstes im Aufbau, damit es HINTER allem liegt. */
|
||
const schatten = el('span', 'kachel__wasserzeichen');
|
||
schatten.append(zeichenBauen(b.zeichen));
|
||
a.append(schatten);
|
||
|
||
const zeichen = el('span', 'kachel__zeichen');
|
||
zeichen.append(zeichenBauen(b.zeichen));
|
||
a.append(zeichen);
|
||
|
||
const text = el('span', 'kachel__text');
|
||
text.append(el('span', 'kachel__name', b.name));
|
||
text.append(el('span', 'kachel__unter', b.unter));
|
||
a.append(text);
|
||
|
||
const zahl = zahlen.get(b.name);
|
||
|
||
/* Das Merkzeichen oben in der Kachel. Es sitzt im Link und nicht
|
||
im Text, damit es sich absolut an der Kachelecke ausrichten
|
||
kann statt im Fluss mitzuschwimmen. */
|
||
if (zahl && zahl.merk > 0) {
|
||
const m = window.Bereiche.merkzeichenBauen(zahl.merk, 'merk');
|
||
m.classList.add('kachel__merk');
|
||
a.append(m);
|
||
/* Die Kachel merkt sich das: Zahl und Pfeil ruecken dann nach
|
||
unten, damit sie dem Zeichen nicht ins Gehege kommen. Die
|
||
Browserpruefung hat genau diese Ueberdeckung gemeldet -- mit
|
||
blossem Auge waren es zwei Pixel. */
|
||
li.dataset.merk = 'ja';
|
||
}
|
||
|
||
/* Besteht die Zahl NUR aus Markierungen, waere sie dieselbe
|
||
Auskunft ein zweites Mal: Das Zeichen sagt bereits "hier ist
|
||
etwas", und bei mehreren steht die Anzahl darin. Dann lieber
|
||
der Pfeil -- zwei Marken uebereinander auf einer Kachel von
|
||
achtzig Pixeln sind Unruhe ohne Gewinn. */
|
||
if (zahl && zahl.n > 0 && zahl.n > zahl.merk) {
|
||
const marke = el('span', 'kachel__zahl', String(zahl.n));
|
||
if (zahl.warn) marke.dataset.warn = 'ja';
|
||
/* Vorlesende Programme bekommen einen Satz statt einer nackten
|
||
Zahl -- "Aufgaben, 3" sagt nichts. */
|
||
marke.setAttribute('aria-label', `${zahl.n} offene Punkte in ${b.name}`);
|
||
a.append(marke);
|
||
} else {
|
||
a.append(el('span', 'kachel__pfeil', '→'));
|
||
}
|
||
|
||
li.append(a);
|
||
liste.append(li);
|
||
}
|
||
abschnitt.append(liste);
|
||
|
||
/* Das Aufklappen laeuft ueber ein Merkmal am Abschnitt, nicht ueber
|
||
`hidden` an der Liste: So kann das CSS den Uebergang zeichnen
|
||
und den Pfeil drehen, ohne dass das Skript etwas davon wissen
|
||
muss. */
|
||
const stand = (istZu) => {
|
||
abschnitt.dataset.zu = istZu ? 'ja' : 'nein';
|
||
kopf.setAttribute('aria-expanded', istZu ? 'false' : 'true');
|
||
liste.hidden = istZu;
|
||
};
|
||
stand(zu);
|
||
kopf.addEventListener('click', () => {
|
||
zu = !zu;
|
||
stand(zu);
|
||
try { localStorage.setItem(schluessel, zu ? 'zu' : 'auf'); } catch { /* egal */ }
|
||
});
|
||
|
||
ziel.append(abschnitt);
|
||
}
|
||
/* KEIN eigener Aufruf mehr: kopf.js stellt das Licht einmal fuer das
|
||
ganze Dokument scharf, und der Zuhoerer dort erfasst auch Kacheln,
|
||
die erst hier entstehen. Ein zweiter Aufruf an dieser Stelle
|
||
haette zwei Zuhoerer auf denselben Bewegungen bedeutet -- doppelte
|
||
Arbeit bei jedem Zeigerzucken. */
|
||
}
|
||
|
||
/* ---------- Das Licht folgt dem Zeiger -----------------------------------
|
||
|
||
Die Kachel trägt einen weichen Lichtfleck. Er sitzt ruhend oben links
|
||
und wandert unter dem Zeiger mit. Kein Blinken, keine Bewegung des
|
||
Inhalts -- es wird nur an einer anderen Stelle heller. Genau das ist
|
||
der Unterschied zwischen "sieht gut aus" und "fühlt sich gut an".
|
||
|
||
EIN Zuhörer für alle Kacheln statt siebzehn einzelner. Und in einem
|
||
Bild je Rahmen: pointermove feuert dutzendfach je Sekunde; wer bei
|
||
jedem Ereignis in den Stil schreibt, lässt den Browser das Bild
|
||
mehrfach umsonst neu berechnen.
|
||
|
||
Auf Fingerbedienung und bei "weniger Bewegung" bleibt es ganz aus --
|
||
dort gibt es keinen Zeiger, dem etwas folgen könnte. */
|
||
/* lichtFolgen steht seit dem 01.09.2026 in kopf.js und gilt fuer ALLE
|
||
Karten im Workspace, nicht nur fuer die Kacheln hier. Eine zweite
|
||
Fassung an dieser Stelle haette bedeutet: zwei Zuhoerer auf
|
||
denselben Bewegungen, und der Fehler waere erst aufgefallen, wenn
|
||
jemand die eine geaendert haette und sich nichts tat. */
|
||
|
||
|
||
/* ---------- Die Begrüßung ------------------------------------------------
|
||
|
||
Sie soll in drei Zeilen sagen, was man beim Reinkommen wissen will:
|
||
WANN ist es, WER bin ich, WIE steht es. Vorher stand dort "Angemeldet"
|
||
-- eine Zeile, die nichts hinzufügt, weil oben rechts der eigene Name
|
||
mit Rolle steht. */
|
||
|
||
const WOCHENTAGE = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch',
|
||
'Donnerstag', 'Freitag', 'Samstag'];
|
||
const MONATE = ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli',
|
||
'August', 'September', 'Oktober', 'November', 'Dezember'];
|
||
|
||
/* Kalenderwoche nach ISO 8601 -- die Woche, in der der Donnerstag liegt.
|
||
Selbst gerechnet, weil eine falsche KW schlimmer ist als gar keine
|
||
und dafür keine fremde Bibliothek in die Seite gehört. */
|
||
function kalenderwoche(d) {
|
||
const t = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
|
||
t.setUTCDate(t.getUTCDate() + 4 - (t.getUTCDay() || 7));
|
||
const anfang = new Date(Date.UTC(t.getUTCFullYear(), 0, 1));
|
||
return Math.ceil(((t - anfang) / 86400000 + 1) / 7);
|
||
}
|
||
|
||
/* Manche Tage haben einen Charakter, und den zu benennen kostet nichts.
|
||
Bewusst NICHT für jeden Tag -- ein Wort, das immer da steht, liest
|
||
nach drei Tagen niemand mehr. */
|
||
const TAGESMARKER = {
|
||
1: 'Neue Woche',
|
||
5: 'Endspurt',
|
||
6: 'Wochenende',
|
||
0: 'Wochenende',
|
||
};
|
||
|
||
/* ---------- Die Uhr in der Begrüßungskachel -------------------------
|
||
Zwei Ringe um dieselbe Mitte: der äußere zeigt die Minute in der
|
||
Stunde, der innere die Sekunde. Beides als SVG-Kreis mit
|
||
`stroke-dasharray` -- ein Kegelverlauf (conic-gradient) hätte an
|
||
der Naht eine sichtbare Kante, ein Kreisbogen nicht.
|
||
|
||
UMFANG = 2 * pi * 44 = 276.46. Die Zahl steht hier ausgerechnet und
|
||
nicht als Formel im Stil: Ändert jemand den Radius im HTML, fällt
|
||
der Ring sofort auf, statt sich stillschweigend zu verrechnen.
|
||
|
||
KEIN setInterval(…, 1000). Ein Takt von genau einer Sekunde läuft
|
||
mit der Zeit aus dem Tritt (jeder Durchlauf kostet ein paar
|
||
Millisekunden), und die Anzeige springt dann gelegentlich zwei
|
||
Sekunden weiter. Stattdessen wird jedes Mal bis zur NÄCHSTEN vollen
|
||
Sekunde gewartet -- die Anzeige bleibt für immer im Takt der
|
||
Systemuhr. */
|
||
/* Drei Umfaenge, ausgerechnet und nicht als Formel im Stil: Aendert
|
||
jemand einen Radius im HTML, faellt der Ring sofort auf, statt sich
|
||
stillschweigend zu verrechnen. U = 2 * pi * r */
|
||
const UMFANG = { sekunde: 285.88, minute: 241.90, stunde: 197.92 }; // r = 45.5 / 38.5 / 31.5
|
||
|
||
function uhrStarten() {
|
||
const zeit = $('uhr-zeit'), sek = $('uhr-sek');
|
||
const ringe = {
|
||
stunde: $('uhr-stunde'), minute: $('uhr-minute'), sekunde: $('uhr-sekunde'),
|
||
};
|
||
if (!zeit || !ringe.sekunde) return;
|
||
const p = (n) => String(n).padStart(2, '0');
|
||
|
||
/* Voll = kein Versatz. Die Ringe wachsen im Uhrzeigersinn, weil das
|
||
SVG im Stil um 90 Grad zurueckgedreht ist. */
|
||
/* ALLE DREI LAGEN EINES RINGES AUF EINMAL (07.09.2026).
|
||
|
||
Seit jeder Ring aus Schatten, Koerper und Oberkante besteht,
|
||
muessen alle drei denselben Stand haben. Sie tragen dafuer
|
||
`data-ring`; einzeln gepflegte Verweise waeren drei Stellen, an
|
||
denen man eine vergessen kann -- und ein Schatten, der stehen
|
||
bleibt, sieht sofort kaputt aus. */
|
||
const lagen = {};
|
||
for (const welcher of ['sekunde', 'minute', 'stunde']) {
|
||
lagen[welcher] = [...document.querySelectorAll(`[data-ring="${welcher}"]`)];
|
||
}
|
||
const setze = (welcher, anteil) => {
|
||
const wert = String(UMFANG[welcher] * (1 - anteil));
|
||
for (const el of lagen[welcher]) el.style.strokeDashoffset = wert;
|
||
};
|
||
|
||
const takt = () => {
|
||
const j = new Date();
|
||
const std = j.getHours(), min = j.getMinutes(), sk = j.getSeconds();
|
||
zeit.textContent = `${p(std)}:${p(min)}`;
|
||
sek.textContent = p(sk);
|
||
|
||
/* JEDER RING LAEUFT WEICH IN SEINER EIGENEN EINHEIT.
|
||
|
||
Die Sekunde springt (sie ist die Anzeige selbst), Minute und
|
||
Stunde nicht: Ein Minutenring, der einmal pro Minute einen
|
||
Sprung macht, sieht aus wie eine haengende Anzeige. Er bekommt
|
||
deshalb die Sekunden anteilig dazu -- und die Stunde die
|
||
Minuten. Genau das hat Filipe verlangt ("die minuten sollen
|
||
auch den minuten entsprechen mitlaufen").
|
||
|
||
Die Stunde ueber ZWOELF, nicht ueber 24: Ein Zifferblatt hat
|
||
zwoelf Stunden, und der Ring soll sich wie eines lesen. */
|
||
setze('sekunde', sk / 60);
|
||
/* Die Perle sitzt auf dem Kopf des Sekundenbogens. Sechs Grad je
|
||
Sekunde, im selben Takt wie die Anzeige -- sie springt also
|
||
mit, statt weich zu gleiten. Das ist ehrlicher (die Anzeige ist
|
||
digital) und kostet EINE Bildberechnung je Sekunde statt
|
||
sechzig. Auf einer Seite, die stundenlang offen steht, ist das
|
||
der Unterschied zwischen einem warmen und einem kalten
|
||
Rechner. */
|
||
const perle = $('uhr-perle');
|
||
if (perle) perle.style.transform = `rotate(${sk * 6}deg)`;
|
||
/* Die beiden anderen Koepfe laufen mit IHREN Ringen -- die Minute
|
||
nimmt die Sekunden anteilig mit, die Stunde die Minuten. Sonst
|
||
staende der Kopf neben dem Ende seines Bogens, und genau das
|
||
sieht man sofort. */
|
||
const perleMin = $('uhr-perle-minute');
|
||
if (perleMin) perleMin.style.transform = `rotate(${(min + sk / 60) * 6}deg)`;
|
||
const perleStd = $('uhr-perle-stunde');
|
||
if (perleStd) perleStd.style.transform = `rotate(${((std % 12) + min / 60) * 30}deg)`;
|
||
setze('minute', (min + sk / 60) / 60);
|
||
setze('stunde', ((std % 12) + min / 60) / 12);
|
||
|
||
/* Der volle Zeitpunkt fuer Vorleseprogramme -- sichtbar steht dort
|
||
nur die Uhrzeit, vorgelesen wird der ganze Satz. */
|
||
const uhr = $('uhr');
|
||
if (uhr) uhr.setAttribute('aria-label', `${p(std)}:${p(min)} Uhr`);
|
||
|
||
/* KEIN setInterval(1000). Ein fester Takt laeuft mit der Zeit aus
|
||
dem Tritt (jeder Durchlauf kostet Millisekunden) und
|
||
ueberspringt dann gelegentlich eine Sekunde. Gewartet wird bis
|
||
zur naechsten VOLLEN Sekunde -- die Anzeige bleibt fuer immer im
|
||
Takt der Systemuhr. */
|
||
setTimeout(takt, 1000 - (Date.now() % 1000) + 8);
|
||
};
|
||
takt();
|
||
}
|
||
|
||
function grussZeigen() {
|
||
const jetzt = new Date();
|
||
const stunde = jetzt.getHours();
|
||
|
||
$('datum').textContent =
|
||
`${WOCHENTAGE[jetzt.getDay()]}, ${jetzt.getDate()}. ${MONATE[jetzt.getMonth()]}`
|
||
+ ` · KW ${kalenderwoche(jetzt)}`;
|
||
|
||
const marker = TAGESMARKER[jetzt.getDay()];
|
||
const mEl = $('tagesmarker');
|
||
if (marker) { mEl.textContent = marker; mEl.hidden = false; }
|
||
|
||
/* Feiner gestuft als vorher: Wer um kurz nach Mitternacht reinkommt,
|
||
liest nicht "Guten Abend", und wer um 13 Uhr kommt, nicht "Morgen". */
|
||
const tageszeit =
|
||
stunde < 5 ? 'Noch wach' :
|
||
stunde < 11 ? 'Guten Morgen' :
|
||
stunde < 14 ? 'Guten Mittag' :
|
||
stunde < 18 ? 'Guten Tag' :
|
||
stunde < 22 ? 'Guten Abend' : 'Späten Abend';
|
||
|
||
/* Der Name ist das Wichtigste an der Zeile und wird deshalb getrennt
|
||
ausgezeichnet -- der Gruß leiser, der Name kräftig. */
|
||
const g = $('gruss');
|
||
g.textContent = '';
|
||
g.append(el('span', 'gruss__wort', tageszeit + ', '));
|
||
g.append(el('span', 'gruss__name', ich.name));
|
||
|
||
/* Die Plakette: das eigene Bild, sonst der erste Buchstabe in der
|
||
Farbe der Rolle. Dieselbe Zuordnung wie im Kopf der Seite, damit
|
||
nichts auseinanderläuft.
|
||
|
||
DAS BILD FEHLTE HIER BIS ZUM 01.09.2026 -- und das ist ein
|
||
lehrreicher Fehler: Der Server liefert es längst mit, und im
|
||
Quelltext dort steht sogar ausdrücklich "es steht in der
|
||
Kopfleiste JEDER Seite UND IN DER BEGRÜSSUNG". Die Absicht war
|
||
aufgeschrieben, die Hälfte davon nie gebaut. Aufgefallen ist es
|
||
nicht, weil ein Buchstabe in einem Kreis nicht falsch aussieht --
|
||
nur eben nicht wie man selbst.
|
||
|
||
Ein Kommentar ist kein Beweis. Deshalb prüft pruef-start-ansicht
|
||
das jetzt mit einem echten Bild nach. */
|
||
const p = $('plakette');
|
||
p.textContent = '';
|
||
p.dataset.rolle = ich.rolle; /* die Plakette bleibt DEINE */
|
||
if (ich.bild) {
|
||
const b = el('img', 'willkommen__foto');
|
||
b.src = ich.bild;
|
||
b.alt = '';
|
||
/* Geht das Bild verloren (gelöschte Datei, kein Netz), erscheint
|
||
sonst ein kaputtes Bildsymbol. Dann lieber zurück zum
|
||
Buchstaben -- der ist immer richtig. */
|
||
b.addEventListener('error', () => {
|
||
b.remove();
|
||
p.textContent = (ich.name || '?').trim().charAt(0).toUpperCase();
|
||
});
|
||
p.append(b);
|
||
} else {
|
||
p.textContent = (ich.name || '?').trim().charAt(0).toUpperCase();
|
||
}
|
||
|
||
/* Die Zeile unter dem Gruss sagt, WESSEN Arbeitsplatz man sieht.
|
||
Bei der eigenen Sicht wie bisher die eigene Rolle; bei fremder
|
||
Sicht der Name davor -- sonst liest man "Creator, dein eigener
|
||
Bereich" und haelt es fuer die eigene Lage. */
|
||
$('rollentext').textContent = ich.sicht
|
||
? `Arbeitsplatz von ${ich.sicht.name} · ${ROLLENTEXT[zeigt.rolle] || zeigt.rolle}`
|
||
: (ROLLENTEXT[ich.rolle] || ich.rolle);
|
||
|
||
/* Datum und Rolle in der Anrede -- wie im Vorbild, wo unter "Noch
|
||
wach, Dogi" das Datum steht und daneben "ADMIN & PARTNER".
|
||
|
||
Das Datum steht damit ZWEIMAL auf der Kachel: oben in der
|
||
Zierzeile mit KW, hier ohne. Das ist Absicht und keine Doppelung:
|
||
Oben gehoert es zur Tafel ("welcher Tag ist heute"), hier zur
|
||
Anrede ("wann begruesse ich dich"). Im Vorbild ist es genauso,
|
||
und es liest sich dort natuerlich. Faellt es Filipe als Doppelung
|
||
auf, kommt die Zierzeile oben weg -- nicht diese Zeile. */
|
||
const jetzt2 = new Date();
|
||
const dat = $('zgruss-datum');
|
||
if (dat) {
|
||
dat.textContent = `${WOCHENTAGE[jetzt2.getDay()]}, `
|
||
+ `${jetzt2.getDate()}. ${MONATE[jetzt2.getMonth()]}`;
|
||
}
|
||
const rolleEl = $('zgruss-rolle');
|
||
if (rolleEl) {
|
||
rolleEl.textContent = (ROLLEN_KURZ[zeigt.rolle] || zeigt.rolle || '').toUpperCase();
|
||
rolleEl.hidden = false;
|
||
}
|
||
}
|
||
|
||
/* Kurzform der Rolle fuer die Plakette neben dem Namen. Der lange
|
||
Text ("Gesamtuebersicht und Freigaben") steht schon darunter --
|
||
hier braucht es das Wort, nicht die Erklaerung. */
|
||
const ROLLEN_KURZ = {
|
||
admin: 'DogFather', manager: 'Manager', scout: 'Scout',
|
||
creator: 'Creator', spicy: 'Spicy Media',
|
||
};
|
||
|
||
/* Ein Satz zur Lage -- gerechnet aus denselben Hinweisen, die darunter
|
||
stehen. Keine zweite Zählung, sonst gäbe es irgendwann zwei
|
||
Wahrheiten auf einem Bildschirm. */
|
||
function lageZeigen() {
|
||
const el2 = $('lage');
|
||
const gesamt = hinweise.reduce((s, h) => s + h.anzahl, 0);
|
||
const dringend = hinweise.filter((h) => h.stufe === 'warnung')
|
||
.reduce((s, h) => s + h.anzahl, 0);
|
||
|
||
let satz, stufe;
|
||
if (dringend > 0) {
|
||
satz = `${dringend} ${dringend === 1 ? 'Sache ist' : 'Sachen sind'} überfällig – das zuerst.`;
|
||
stufe = 'warnung';
|
||
} else if (gesamt > 0) {
|
||
satz = `${gesamt} ${gesamt === 1 ? 'Punkt steht' : 'Punkte stehen'} an, nichts davon brennt.`;
|
||
stufe = 'offen';
|
||
} else {
|
||
satz = 'Nichts liegt an – guter Tag zum Vorarbeiten.';
|
||
stufe = 'ruhig';
|
||
}
|
||
el2.textContent = satz;
|
||
el2.dataset.stufe = stufe;
|
||
el2.hidden = false;
|
||
|
||
standSetzen('offen', gesamt === 0 ? '0' : String(gesamt), stufe);
|
||
}
|
||
|
||
/* ---------- Der Ring links ----------------------------------------------
|
||
Nach dem Vorbild von VanVans Werktisch. Zwei Bahnen:
|
||
|
||
INNEN eine feine Bahn mit EINEM Segment je Person (bzw. je
|
||
Stunde), eingefaerbt nach Lage -- dort sieht man jedes
|
||
Einzelne.
|
||
AUSSEN ein durchgehender Bogen, der GENAU auf der Prozentzahl in
|
||
der Mitte steht.
|
||
|
||
Die Trennung ist aus dem Vorbild uebernommen, samt Begruendung:
|
||
Dort zeigte der Ring erst die 162 Produkte je einzeln, und der
|
||
Anteil des Blauen entsprach dann der Zahl der fertigen AKTEN (44 %),
|
||
waehrend daneben die Zahl der erfuellten ANGABEN (91 %) stand. Zwei
|
||
Auskuenfte, die sich widersprechen, sind schlimmer als eine.
|
||
Deshalb: der Bogen sagt dasselbe wie die Zahl, immer. */
|
||
|
||
const RING_FARBE = {
|
||
frei: '#57b89b', /* nichts offen -- gedaempftes Petrol, kein Grasgruen */
|
||
dran: '#7fc4f2', /* heute versorgt */
|
||
offen: '#ff6b74', /* liegt liegen */
|
||
};
|
||
|
||
function bogen(cx, cy, r, von, bis) {
|
||
const x1 = cx + r * Math.cos(von), y1 = cy + r * Math.sin(von);
|
||
const x2 = cx + r * Math.cos(bis), y2 = cy + r * Math.sin(bis);
|
||
const gross = bis - von > Math.PI ? 1 : 0;
|
||
return `M ${x1.toFixed(2)} ${y1.toFixed(2)} A ${r} ${r} 0 ${gross} 1 ${x2.toFixed(2)} ${y2.toFixed(2)}`;
|
||
}
|
||
const svgEl = (name, attr) => {
|
||
const n = document.createElementNS('http://www.w3.org/2000/svg', name);
|
||
for (const [k, v] of Object.entries(attr)) n.setAttribute(k, v);
|
||
return n;
|
||
};
|
||
|
||
function ringZeichnen(daten) {
|
||
const svg = $('zring-bild');
|
||
if (!svg) return;
|
||
svg.replaceChildren();
|
||
const segmente = daten.segmente || [];
|
||
|
||
/* Der Verlauf fuer den Fortschrittsbogen -- eine Reise von Rot nach
|
||
Babyblau, dieselben zwei Farben wie in der Uhr daneben und im
|
||
Rahmen der Kachel. Keine dritte Handschrift. */
|
||
const defs = svgEl('defs', {});
|
||
const lauf = svgEl('linearGradient', { id: 'zring-lauf', x1: '0', y1: '0', x2: '1', y2: '1' });
|
||
for (const [o, f] of [['0%', '#ff5f6d'], ['46%', '#c07ad0'], ['100%', '#7fc4f2']]) {
|
||
lauf.appendChild(svgEl('stop', { offset: o, 'stop-color': f }));
|
||
}
|
||
defs.appendChild(lauf);
|
||
svg.appendChild(defs);
|
||
|
||
/* 1. Die leere Bahn zuerst -- damit der Ring auch bei 0 % ein Ring
|
||
ist und nicht ein Punkt. */
|
||
svg.appendChild(svgEl('path', {
|
||
d: bogen(150, 150, 128, 0, Math.PI * 1.9999),
|
||
stroke: 'rgba(120, 150, 185, .16)', 'stroke-width': '9',
|
||
fill: 'none', 'stroke-linecap': 'round',
|
||
}));
|
||
|
||
/* 2. Ein Segment je Person (oder Stunde), innen. Mit Luecke
|
||
dazwischen, sonst verschmelzen sie zu einem Ring und man
|
||
zaehlt nichts mehr. */
|
||
if (segmente.length) {
|
||
const schritt = (Math.PI * 2) / segmente.length;
|
||
const luecke = Math.min(schritt * 0.22, 0.06);
|
||
segmente.forEach((s, i) => {
|
||
const von = i * schritt + luecke / 2;
|
||
const bis = (i + 1) * schritt - luecke / 2;
|
||
const st = svgEl('path', {
|
||
d: bogen(150, 150, 106, von, bis),
|
||
stroke: RING_FARBE[s.stufe] || '#8fb3cf',
|
||
'stroke-width': '7', fill: 'none', 'stroke-linecap': 'round',
|
||
opacity: s.stufe === 'offen' ? '.95' : '.72',
|
||
});
|
||
/* Der Name als Sprechblase. Nur der Vorname, und nur das, was
|
||
der Server ohnehin geschickt hat -- der Browser erfaehrt
|
||
nichts, was er nicht sehen darf. */
|
||
const titel = document.createElementNS('http://www.w3.org/2000/svg', 'title');
|
||
titel.textContent = s.name || '';
|
||
st.appendChild(titel);
|
||
svg.appendChild(st);
|
||
});
|
||
}
|
||
|
||
/* 3. Der Fortschrittsbogen aussen, genau auf der Prozentzahl. */
|
||
const anteil = Math.max(0, Math.min(100, Number(daten.prozent) || 0));
|
||
const bis = (anteil / 100) * Math.PI * 2;
|
||
if (bis > 0.01) {
|
||
const d = bogen(150, 150, 128, 0, Math.min(bis, Math.PI * 1.9999));
|
||
svg.appendChild(svgEl('path', {
|
||
d, stroke: 'url(#zring-lauf)', 'stroke-width': '9',
|
||
fill: 'none', 'stroke-linecap': 'round',
|
||
}));
|
||
/* Ein heller Punkt an der Spitze macht aus dem Bogen einen
|
||
Zeiger -- er markiert den Stand so genau, wie die Zahl es sagt. */
|
||
const px = 150 + 128 * Math.cos(bis), py = 150 + 128 * Math.sin(bis);
|
||
svg.appendChild(svgEl('circle', { cx: px, cy: py, r: '3.4', fill: '#eaf4ff' }));
|
||
}
|
||
|
||
$('zring-prozent').textContent = `${anteil} %`;
|
||
$('zring-unter').textContent = daten.titel || '';
|
||
}
|
||
|
||
async function zentraleLaden() {
|
||
let daten;
|
||
try {
|
||
const antwort = await fetch('/workspace/api/zentrale', { credentials: 'same-origin' });
|
||
if (!antwort.ok) return;
|
||
daten = await antwort.json();
|
||
} catch { return; } /* Der Rest der Kachel steht trotzdem. */
|
||
|
||
ringZeichnen(daten);
|
||
|
||
/* Die drei Zahlen unter dem Titel kommen bei Leitung aus derselben
|
||
Antwort -- eine Abfrage, eine Wahrheit. Fuer die Tagesansicht
|
||
fuellen `lageZeigen` und `tagestermineLaden` sie weiter; deshalb
|
||
wird hier nur ueberschrieben, was der Server auch geschickt hat. */
|
||
const zahlen = daten.zahlen || [];
|
||
const felder = ['heute', 'naechster', 'offen'];
|
||
zahlen.slice(0, 3).forEach((z, i) => {
|
||
const wert = $('stand-' + felder[i]);
|
||
const schild = wert?.parentElement?.querySelector('.stand__schild');
|
||
if (!wert || !schild) return;
|
||
wert.textContent = String(z.wert);
|
||
schild.textContent = z.schild;
|
||
/* Die Farbe folgt der Bedeutung, nicht der Position: Was liegen
|
||
bleibt, ist rot -- egal an welcher Stelle es steht. */
|
||
const kasten = wert.closest('.stand');
|
||
if (kasten) {
|
||
kasten.dataset.stufe = /liegt|offen|fehlt/i.test(z.schild)
|
||
? (Number(z.wert) > 0 ? 'warnung' : 'ruhig') : '';
|
||
}
|
||
});
|
||
if (zahlen.length) {
|
||
standVonZentrale = true;
|
||
$('tagesstand').hidden = false;
|
||
}
|
||
}
|
||
|
||
/* ---------- Die drei Ablesungen in der Konsolenmitte --------------------- */
|
||
|
||
/* EINE Funktion fuer alle drei, und sie wird von den STELLEN gerufen,
|
||
die die Zahl ohnehin schon haben -- lageZeigen() und
|
||
tagestermineLaden(). Hier wird nichts nachgezaehlt: Zwei Rechenwege
|
||
fuer dieselbe Zahl laufen auseinander, und dann stehen zwei
|
||
Wahrheiten auf einem Bildschirm.
|
||
|
||
Der Kasten geht auf, sobald die ERSTE Zahl da ist. Er wartet nicht
|
||
auf beide: Wer nichts Offenes hat, aber drei Calls, soll seinen Tag
|
||
sehen, auch wenn die andere Abfrage haengt. Genau diesen Fehler hat
|
||
die Kachel darunter schon einmal gemacht. */
|
||
/* WER DIE FELDER BESETZT HAT, BEHAELT SIE (08.09.2026).
|
||
|
||
Seit der Ring da ist, schreiben ZWEI Quellen in dieselben drei
|
||
Felder: `zentraleLaden` (bei Leitung: im Team / heute dran / liegt
|
||
liegen) und `lageZeigen` + `tagestermineLaden` (Heute / Als
|
||
Naechstes / Offen). Beide laufen nebeneinander los -- wer zuletzt
|
||
ankommt, haette gewonnen, und das waere bei jedem Laden eine andere.
|
||
|
||
Ein Bildschirm, auf dem "heute dran: 2" steht und beim naechsten
|
||
Aufruf "Heute: 5", ist schlimmer als einer, der weniger zeigt.
|
||
Deshalb: Hat die Zentrale die Felder belegt, ruehrt sie niemand
|
||
mehr an. Die Reihenfolge des Eintreffens spielt dann keine Rolle. */
|
||
let standVonZentrale = false;
|
||
|
||
function standSetzen(welches, wert, stufe) {
|
||
if (standVonZentrale) return;
|
||
const feld = $('stand-' + welches);
|
||
if (!feld) return;
|
||
feld.textContent = wert;
|
||
if (stufe) feld.closest('.stand')?.setAttribute('data-stufe', stufe);
|
||
const kasten = $('tagesstand');
|
||
if (kasten) kasten.hidden = false;
|
||
}
|
||
|
||
/* ---------- Laden -------------------------------------------------------- */
|
||
|
||
async function laden() {
|
||
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;
|
||
}
|
||
|
||
/* Die Sicht steht in der Antwort von /api/ich. Ohne sie ist `zeigt`
|
||
schlicht die eigene Person -- der Normalfall. */
|
||
zeigt = ich.sicht || ich;
|
||
|
||
window.werZeigen?.(ich);
|
||
grussZeigen();
|
||
uhrStarten();
|
||
|
||
/* Die Hinweise zuerst -- die Kacheln brauchen ihre Zahlen.
|
||
Die Tagestermine laufen daneben her: Sie sind die rechte Hälfte
|
||
derselben Kachel, aber niemand soll auf sie warten. Scheitern
|
||
sie, steht die linke Hälfte trotzdem. */
|
||
/* Der Ring laeuft daneben her, wie die Tagestermine: Er ist die
|
||
linke Spalte der Kachel, aber niemand soll auf ihn warten.
|
||
Scheitert er, steht der Rest trotzdem. */
|
||
const ringLaeuft = zentraleLaden();
|
||
|
||
const termineLaeuft = tagestermineLaden();
|
||
await hinweiseLaden();
|
||
/* KEIN `await ringLaeuft` HIER (08.09.2026).
|
||
|
||
Genau das stand kurz an dieser Stelle, und pruef-start-ansicht
|
||
wurde daraufhin dreimal rot -- nicht am Ring, sondern am
|
||
MAUS-LICHT auf den Kacheln weiter unten. Der Grund: Alles, was
|
||
nach diesem `await` kommt, wartet mit. Der Ring braucht eine
|
||
Server-Abfrage, und in der Zeit war das Licht noch nicht
|
||
eingehaengt, als die Pruefung mit dem Zeiger darueberfuhr.
|
||
|
||
Gemessen, nicht vermutet: derselbe Lauf am alten Stand meldete
|
||
null Befunde, mit dem `await` drei. Ein zusaetzliches `await` in
|
||
einer Ladefolge ist nie nur eine Verzoegerung fuer sich selbst --
|
||
es verzoegert alles dahinter. */
|
||
await termineLaeuft;
|
||
/* Erst jetzt steht fest, wie die Lage ist. */
|
||
lageZeigen();
|
||
bereicheBauen();
|
||
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`,
|
||
/* Diese vier beginnen mit der Zahl, wie alle anderen auch: Die Zeile
|
||
schneidet die führende Zahl ab und zeigt sie getrennt davor. Ein
|
||
Satz, der anders anfängt ("Deine Betreuung hat 3 …"), stünde
|
||
hinter der Marke ein zweites Mal mit der 3 -- kein Fehler, den ein
|
||
Programm meldet, aber einer, den jeder sieht. */
|
||
markiert_live: (n) => `${n} markierte${n === 1 ? 'r Punkt' : ' Punkte'} in der LIVE-Analyse`,
|
||
markiert_content: (n) => `${n} markierte ${n === 1 ? 'Content-Idee' : 'Content-Ideen'}`,
|
||
markiert_community: (n) => `${n} markierte${n === 1 ? 'r Punkt' : ' Punkte'} bei Community`,
|
||
markiert_technik: (n) => `${n} markierte${n === 1 ? 'r Punkt' : ' Punkte'} bei Technik`,
|
||
};
|
||
|
||
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; }
|
||
|
||
hinweise = daten.hinweise || [];
|
||
const liste = $('dran');
|
||
liste.textContent = '';
|
||
|
||
/* DIE KACHEL HAT ZWEI HÄLFTEN (05.09.2026), und keine darf die
|
||
andere mitreißen.
|
||
|
||
Hier stand `if (!hinweise.length) { block.hidden = true; return; }`
|
||
-- solange die Kachel nur die Hinweise enthielt, war das richtig.
|
||
Jetzt stehen rechts die Termine des Tages: Wer nichts Offenes
|
||
hat, aber drei Calls, hätte seinen Tagesplan nicht mehr gesehen.
|
||
Über das Verstecken entscheidet deshalb tagesblickPruefen(),
|
||
wenn BEIDE Seiten geladen sind. */
|
||
$('dran-leer').hidden = hinweise.length > 0;
|
||
if (!hinweise.length) { tagesblickPruefen(); return; }
|
||
|
||
for (const h of hinweise) {
|
||
const li = el('li', 'dran__punkt');
|
||
li.dataset.stufe = h.stufe;
|
||
|
||
/* Ein Hinweis trägt ZEICHEN UND FARBE der Kachel, zu der er führt.
|
||
Dadurch gehört sichtbar zusammen, was zusammengehört: Wer die
|
||
rote Zeile „2 Aufgaben sind überfällig" sieht, erkennt weiter
|
||
unten dieselbe Farbe und dasselbe Zeichen auf der Kachel
|
||
wieder -- und weiß, wohin er muss, bevor er den Text zu Ende
|
||
gelesen hat. Beides kommt aus derselben Zuordnung wie die Zahl
|
||
auf der Kachel, es gibt also keine zweite Liste. */
|
||
const kachel = bereichZu(HINWEIS_ZU_KACHEL[h.art]);
|
||
if (kachel) li.dataset.ton = String(kachel.ton);
|
||
|
||
/* Jeder Hinweis führt irgendwohin -- ein Hinweis ohne Ziel ist
|
||
nur ein schlechtes Gewissen. */
|
||
const a = el('a', 'dran__link');
|
||
a.href = h.ziel;
|
||
|
||
if (kachel) {
|
||
const zeichen = el('span', 'dran__zeichen');
|
||
zeichen.append(zeichenBauen(kachel.zeichen, 'dran__svg'));
|
||
a.append(zeichen);
|
||
}
|
||
|
||
/* Die Zahl steht vorn und getrennt vom Satz. Man liest sie zuerst,
|
||
und beim Überfliegen von sieben Zeilen zählt genau das. */
|
||
a.append(el('span', 'dran__zahl', String(h.anzahl)));
|
||
|
||
const satz = (EINZAHL[h.art] || ((n) => `${n} × ${h.text}`))(h.anzahl);
|
||
/* Die Zahl steht schon davor -- im Satz wäre sie doppelt. */
|
||
a.append(el('span', 'dran__text', satz.replace(/^\d+\s+(×\s+)?/, '')));
|
||
|
||
const pfeil = el('span', 'dran__pfeil', '→');
|
||
pfeil.setAttribute('aria-hidden', 'true');
|
||
a.append(pfeil);
|
||
/* Vorleseprogramme bekommen den ganzen Satz, nicht die Bruchstücke. */
|
||
a.setAttribute('aria-label', satz);
|
||
li.append(a);
|
||
liste.append(li);
|
||
}
|
||
/* Sieben Hinweise untereinander sind eine Wand -- und die
|
||
wichtigsten stehen ohnehin oben (der Server sortiert nach Stufe).
|
||
Sichtbar bleiben vier, der Rest ist einen Klick entfernt.
|
||
Derselbe Helfer wie im Protokoll und im Verlauf. */
|
||
window.klappe?.(liste, $('dran-schalter'), $('dran-schalter-text'));
|
||
tagesblickPruefen();
|
||
}
|
||
|
||
/* ---------- Die rechte Hälfte: was heute ansteht ------------------------
|
||
|
||
Wunsch 05.09.2026: *"rechts in der kachel die termine vom tag."*
|
||
|
||
WARUM NEBEN DEN HINWEISEN: Links steht, was zu TUN ist, rechts,
|
||
was schon FESTSTEHT. Zusammen beantworten sie die Frage, die man
|
||
morgens hat -- und die man vorher nur durch Scrollen beantworten
|
||
konnte.
|
||
|
||
Die Daten kommen aus derselben Schnittstelle wie der Kalender, mit
|
||
`tage=1`. Keine zweite Abfrage, keine zweite Sichtbarkeitsregel:
|
||
Was jemand im Kalender nicht sehen darf, kommt hier gar nicht erst
|
||
an. */
|
||
|
||
let tagesterminGeladen = false;
|
||
|
||
/** Entscheidet, ob die Kachel überhaupt etwas zu zeigen hat.
|
||
*
|
||
* Läuft nach BEIDEN Hälften. Eine Kachel mit zwei leeren Seiten wäre
|
||
* ein Rahmen um nichts -- eine mit einer vollen Seite dagegen ist
|
||
* genau richtig, und welche das ist, steht vorher nicht fest. */
|
||
function tagesblickPruefen() {
|
||
const etwasLinks = hinweise.length > 0;
|
||
const etwasRechts = $('tagestermine').childElementCount > 0;
|
||
/* Solange die Termine noch unterwegs sind, wird nicht versteckt --
|
||
sonst blitzt die Kachel auf und verschwindet wieder. */
|
||
const kachel = $('dran-block');
|
||
if (!tagesterminGeladen) return; /* der Platzhalter haelt die Hoehe */
|
||
|
||
/* Jetzt steht beides fest: Der Platzhalter wird abgeraeumt, und die
|
||
Kachel bleibt nur, wenn sie etwas zu zeigen hat. */
|
||
delete kachel.dataset.laedt;
|
||
kachel.hidden = !etwasLinks && !etwasRechts;
|
||
}
|
||
|
||
/** hh:mm aus einem Zeitstempel, ohne Zeitzonen-Rechnerei.
|
||
* Die Werte kommen als "2026-09-05T14:30" vom Server -- schon in
|
||
* Ortszeit. Ein `new Date()` darauf würde sie ein zweites Mal
|
||
* umrechnen und Termine um Stunden verschieben. */
|
||
const uhrzeit = (beginn) => String(beginn || '').slice(11, 16);
|
||
|
||
const ART_NAME = { call: 'Call', termin: 'Termin', review: 'Review', frist: 'Frist' };
|
||
|
||
async function tagestermineLaden() {
|
||
const liste = $('tagestermine');
|
||
let termine = [];
|
||
try {
|
||
const heute = window.heuteLokal();
|
||
const antwort = await fetch(
|
||
`/workspace/api/termine?von=${heute}&tage=1`, { credentials: 'same-origin' });
|
||
if (antwort.ok) {
|
||
const daten = await antwort.json();
|
||
/* `tage=1` liefert den Zeitraum bis morgen früh -- auf den
|
||
heutigen Tag wird hier noch einmal genau geschnitten. */
|
||
termine = (daten.termine || []).filter((t) => String(t.beginn).slice(0, 10) === heute);
|
||
}
|
||
} catch { /* die linke Hälfte steht trotzdem */ }
|
||
|
||
liste.textContent = '';
|
||
tagesterminGeladen = true;
|
||
|
||
/* Auch der leere Fall wird GESETZT, nicht uebersprungen. Ein Feld,
|
||
das bei null Terminen auf dem Strich stehen bleibt, sieht aus wie
|
||
eine Abfrage, die haengt -- und man wartet auf eine Zahl, die nie
|
||
kommt. "0" ist eine Auskunft, "–" ist keine. */
|
||
if (!termine.length) {
|
||
$('heute-leer').hidden = false;
|
||
standSetzen('heute', '0');
|
||
standSetzen('naechster', '–');
|
||
tagesblickPruefen();
|
||
return;
|
||
}
|
||
$('heute-leer').hidden = true;
|
||
|
||
/* Was ist gerade dran? Der erste Termin, der noch nicht vorbei ist.
|
||
Er wird hervorgehoben -- beim Überfliegen ist genau das die
|
||
Auskunft, die man sucht, und nicht "der erste des Tages". */
|
||
const jetzt = new Date();
|
||
const jetztText = `${String(jetzt.getHours()).padStart(2, '0')}:`
|
||
+ `${String(jetzt.getMinutes()).padStart(2, '0')}`;
|
||
const naechster = termine.find((t) => {
|
||
const ende = new Date(Date.parse(t.beginn.replace(' ', 'T'))
|
||
+ (Number(t.dauer_min) || 30) * 60000);
|
||
return !Number.isNaN(ende.getTime()) ? ende >= jetzt : uhrzeit(t.beginn) >= jetztText;
|
||
});
|
||
|
||
/* Die zwei Ablesungen fuer den Tag -- aus GENAU diesen Werten, die
|
||
die Liste darunter ohnehin benutzt.
|
||
|
||
"Als Naechstes" zeigt den Termin, der noch nicht vorbei ist. Ist
|
||
keiner mehr uebrig, steht dort "fertig" und nicht die Uhrzeit des
|
||
letzten: Ein vergangener Termin unter der Ueberschrift "Als
|
||
Naechstes" ist schlimmer als gar keine Angabe. */
|
||
standSetzen('heute', String(termine.length));
|
||
standSetzen('naechster', naechster ? uhrzeit(naechster.beginn) : 'fertig');
|
||
|
||
/* HÖCHSTENS FÜNF. Zwei Gründe, und beide zählen:
|
||
|
||
Fachlich: Eine Kachel auf der Startseite beantwortet "was kommt
|
||
als Nächstes", nicht "was war alles". Wer zwanzig Termine hat,
|
||
will die nächsten sehen und dann in den Kalender -- eine Liste,
|
||
die den halben Bildschirm füllt, nimmt den Blick auf alles
|
||
darunter.
|
||
|
||
Technisch: Ohne Grenze hängt die Höhe der Kachel an der Zahl der
|
||
Termine, und der reservierte Platz kann nie passen. Der
|
||
Layout-Sprung lag deshalb bei 0,93 -- alles unterhalb rutschte
|
||
beim Laden nach unten. Mit einer Obergrenze ist die Höhe
|
||
planbar.
|
||
|
||
Warum DREI und nicht fuenf: Gemessen wurde die Kachelhoehe --
|
||
ein Termin 169 px, drei 327 px, fuenf 486 px. Je weiter die
|
||
Spanne, desto weniger kann der reservierte Platz passen, und
|
||
desto staerker springt die Seite. Bei drei liegt die Spanne bei
|
||
gut 150 Pixeln statt bei 320.
|
||
|
||
Die weggelassenen werden GENANNT, nicht verschwiegen: Eine
|
||
Liste, die stillschweigend abschneidet, liest sich wie der
|
||
ganze Tag -- und man verpasst den Termin um sechs. */
|
||
const ZEIGE_HOECHSTENS = 3;
|
||
const gezeigt = termine.slice(0, ZEIGE_HOECHSTENS);
|
||
const weitere = termine.length - gezeigt.length;
|
||
|
||
for (const t of gezeigt) {
|
||
const li = el('li', 'tagesliste__punkt');
|
||
li.dataset.art = t.art || 'termin';
|
||
|
||
const beginn = uhrzeit(t.beginn);
|
||
if (t === naechster) li.dataset.naechster = 'ja';
|
||
/* Vorbei heißt nicht weg: Man will sehen, was man heute schon
|
||
hinter sich hat. Es tritt nur zurück. */
|
||
else if (beginn < jetztText) li.dataset.vorbei = 'ja';
|
||
|
||
/* ---- Links: Uhrzeit, Dauerbalken, Dauer ---- */
|
||
const zeit = el('span', 'tagesliste__zeit');
|
||
zeit.append(el('span', 'tagesliste__stunde', beginn));
|
||
|
||
if (t.dauer_min) {
|
||
/* DER BALKEN ZEIGT DIE LÄNGE. Bezugsgröße sind zwei Stunden --
|
||
darüber ist er voll. Das ist keine Skala, sondern ein
|
||
Verhältnis: Man soll auf einen Blick sehen, ob ein Termin
|
||
kurz oder lang ist, ohne "30 min" zu lesen.
|
||
|
||
Zwei Stunden als Vollausschlag, weil darüber im Alltag
|
||
kaum etwas liegt; ein Ganztagestermin würde jeden anderen
|
||
Balken sonst auf einen Strich zusammendrücken. */
|
||
const anteil = Math.max(12, Math.min(100, (Number(t.dauer_min) / 120) * 100));
|
||
const balken = el('span', 'tagesliste__balken');
|
||
balken.style.setProperty('--laenge', `${Math.round(anteil)}%`);
|
||
balken.setAttribute('aria-hidden', 'true'); // die Zahl daneben sagt es
|
||
const laenge = el('span', 'tagesliste__laenge');
|
||
laenge.append(balken, el('span', 'tagesliste__dauer', `${t.dauer_min} min`));
|
||
zeit.append(laenge);
|
||
}
|
||
li.append(zeit);
|
||
|
||
/* ---- Mitte: Titel und Beteiligte ---- */
|
||
const text = el('div', 'tagesliste__text');
|
||
text.append(el('p', 'tagesliste__titel', t.titel));
|
||
|
||
const unten = [];
|
||
if (t.teilnehmer_name) unten.push(t.teilnehmer_name);
|
||
else if (t.creator_name) unten.push(t.creator_name);
|
||
if (t.ort) unten.push(t.ort);
|
||
if (unten.length) text.append(el('p', 'tagesliste__unter', unten.join(' · ')));
|
||
li.append(text);
|
||
|
||
/* ---- Rechts: Art und, beim nächsten, die Jetzt-Marke ---- */
|
||
const rechts = el('div', 'tagesliste__rechts');
|
||
rechts.append(el('span', 'tagesliste__art', ART_NAME[t.art] || t.art || ''));
|
||
/* Steht im Markup immer, wird aber nur beim nächsten Termin
|
||
eingeblendet (CSS). Ein Wort sagt hier mehr als ein hellerer
|
||
Hintergrund -- und ein Vorleseprogramm bekommt es ebenfalls. */
|
||
rechts.append(el('span', 'tagesliste__jetzt', 'Jetzt'));
|
||
li.append(rechts);
|
||
|
||
/* Der ganze Eintrag führt in den Kalender, auf den richtigen Tag.
|
||
Ein Termin, den man nur anschauen kann, ist eine Sackgasse. */
|
||
const link = el('a', 'tagesliste__link');
|
||
link.href = `kalender.html?tag=${String(t.beginn).slice(0, 10)}`;
|
||
link.setAttribute('aria-label',
|
||
`${beginn} Uhr, ${t.titel}${unten.length ? ', ' + unten.join(', ') : ''}`);
|
||
li.append(link);
|
||
|
||
liste.append(li);
|
||
}
|
||
|
||
/* Was nicht mehr hineinpasste, wird gesagt -- mit dem Weg dorthin.
|
||
Eine Liste, die stillschweigend abschneidet, liest sich wie der
|
||
ganze Tag, und man verpasst den Termin um sechs. */
|
||
if (weitere > 0) {
|
||
const mehr = el('li', 'tagesliste__mehr');
|
||
const a = el('a', 'tagesliste__mehr-link',
|
||
`${weitere} weitere${weitere === 1 ? 'r Termin' : ' Termine'} heute →`);
|
||
a.href = 'kalender.html';
|
||
mehr.append(a);
|
||
liste.append(mehr);
|
||
}
|
||
|
||
tagesblickPruefen();
|
||
}
|
||
|
||
/* 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; }
|
||
|
||
/* Sechs Nullen nebeneinander sind kein Bericht, sondern Rauschen.
|
||
Wenn wirklich nichts offen ist, steht dort ein Satz -- und der
|
||
Platz gehört den Bereichen. */
|
||
const summe = ZAHLEN.reduce((n, z) => n + (daten[z.schluessel] ?? 0), 0);
|
||
const block = $('zahlen-block');
|
||
if (!summe) {
|
||
block.dataset.leer = 'ja';
|
||
$('zahlen').textContent = '';
|
||
$('zahlen-leer').hidden = false;
|
||
return;
|
||
}
|
||
block.removeAttribute('data-leer');
|
||
$('zahlen-leer').hidden = true;
|
||
|
||
const liste = $('zahlen');
|
||
liste.textContent = '';
|
||
for (const z of ZAHLEN) {
|
||
const wert = daten[z.schluessel] ?? 0;
|
||
const li = el('li', 'zahl');
|
||
if (z.warn && wert > 0) li.dataset.warn = 'ja';
|
||
li.append(el('span', 'zahl__wert', String(wert)));
|
||
li.append(el('span', 'zahl__name', z.name));
|
||
liste.append(li);
|
||
}
|
||
}
|
||
|
||
laden();
|
||
})();
|