Files
dogfather-universe/workspace/assets/js/leistung.js
T
DogFatherGitandClaude Opus 5 1b2874299e Uhr und Glocke in die Begruessung, Teilen in die Leiste, ein echter Fehler weg
Neun Punkte aus Filipes Bildschirmfotos. Der wichtigste war kein
Aussehen, sondern ein Fehler:

UEBEREINANDERLIEGENDE TEXTE IN DER SICHERUNGSLISTE. Das Datum entstand
aus `name.split('-').slice(1).reverse().join('.')`. Bei
"woechentlich-2026-09-06.db" ging das gut; die Sicherungen vor einem
Umbau heissen aber "vorher-2026-09-02-160012438.db" -- mit Zeitstempel.
Heraus kam "160012438.02.09.2026", dreimal so lang wie die 96-px-Spalte,
und es lief ueber den Nachbartext. Ein Muster, das Bestandteile ZAEHLT
statt sie zu SUCHEN, bricht beim ersten Namen mit einem Teil mehr. Jetzt
ein Suchmuster nach vier-zwei-zwei Ziffern -- und in der CSS eine
Kuerzung, damit der NAECHSTE zu lange Text nur abgeschnitten wird. Eine
Spalte mit fester Breite ohne Kuerzung ist immer eine Zeitbombe.

DIE BEGRUESSUNGSKACHEL. Runde Digitaluhr rechts: zwei Ringe um dieselbe
Mitte -- innen die Sekunde, aussen der Stand der Stunde. Kein
setInterval(1000): Ein fester Takt laeuft mit der Zeit aus dem Tritt und
ueberspringt Sekunden; gewartet wird bis zur naechsten VOLLEN Sekunde.
Die Glocke ist aus der Kopfleiste hierhergezogen -- mit Rueckfall, denn
zwoelf andere Seiten haben diesen Platz nicht. Nebenbei ist die Leiste
damit um ein Element leichter; sie ist am 06.09. schon einmal an einem
sechsten zerbrochen.

TEILEN-KNOPF in der Leiste, nur fuer Scout, Manager und DogFather.
Geteilt wird der EINGANG, nicht die aktuelle Seite: Ein Link auf
bereich.html?b=schutz schickt jemanden auf eine Seite, die er nicht
sehen darf. Wo es navigator.share gibt, wird es benutzt; sonst
Zwischenablage; wo beides fehlt, erscheint der Knopf gar nicht -- ein
dritter Ausgang statt einer Schaltflaeche, die nichts tut.

WEITER: Kachelreihenfolge Steckbrief -> Profile -> Zahlen. Die
Tagesliste laesst sich zuklappen und zeigt dann SIEBEN Tage (die Woche,
nicht die vier von ueberall sonst). Dialoge, Automationen-Karten,
Sicherungsblock, KI-Kasten und Call-Karten bekommen dasselbe Material
wie die Kacheln -- Leuchtschiene, Materialstaerke, Glanz.

Profilbilder brauchten nichts: Der Weg gibt es fuer jede Rolle bereits
(steckbrief.html fuer die Betreuung, derselbe Block auf profil.html fuer
Creator, Hochladen schreibt immer auf req.person.id).

ZWEI EIGENE FEHLER, beide gemessen statt vermutet:
  - Die Koernung lag in vier neuen Bloecken auf DERSELBEN Ebene wie die
    Spiegelung und erbte deren 50 % Deckkraft. Gemessen rgb(56,44,58)
    statt rgb(20,26,38) -- die Karten sahen durchsichtig aus, obwohl sie
    zu 95 % decken. Derselbe Fehler wie heute Nachmittag an der
    Anmeldekarte. Koernung gehoert auf eine eigene Ebene mit overlay.
  - `.glocke-platz:empty { display: none }` liess die Kachel wachsen,
    sobald die Glocke geladen war. Layout-Sprung von start.html: 0,708.
    Platz wird jetzt reserviert -> 0,473.

pruef-struktur: `-breit` gehoert in die Stufen-Ausnahme. Schaerfe kostet
Bytes (hohe Frequenzen lassen sich nicht wegrechnen), die mittlere Stufe
wuchs auf 230-300 KB. Die Regel bleibt inhaltlich: gross nur, WENN eine
kleinere Stufe daneben steht. Die Verkettung der beiden replace() waere
ein stiller Fehler gewesen -- fuer uhd haette sie nach `-schmal` gesucht.

Gruen: kopf-messen (3), breiten (23), glocke (26), css-klassen (15),
struktur (32), buehne (38), start-ansicht (136), handy (59),
formulare (19), barrierefrei (18), lesbarkeit (14), tempo (8).

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-07 03:40:49 +02:00

607 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =====================================================================
leistung.js — die Zahlen über das Geschäft.
Stufe 1 des Plans vom 31.08.2026. Der Server rechnet (siehe
workspace-leistung.js), diese Datei zeigt und nimmt entgegen.
DREI DINGE, die jede Zahl hier mitbringt -- sonst fliegt sie raus:
1. den WERT
2. den VERGLEICH zur Vorwoche ("+23 %")
3. den VERLAUF als kleine Linie
Eine Zahl ohne diese drei ist eine Eitelkeitszahl. Sie sieht nach
Auskunft aus, und man kann nichts entscheiden.
===================================================================== */
(() => {
'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 LEITUNG = new Set(['admin', 'manager']);
let ich = null;
let creator = [];
let offenerCreator = 0;
let stand = null;
let importText = '';
let importSpalten = null;
async function hole(weg, einst) {
const a = await fetch(weg, { credentials: 'same-origin', ...einst });
if (a.status === 401) { location.assign('/workspace/'); throw new Error('weg'); }
return a;
}
const melde = (t) => { $('fehler').textContent = t || ''; };
/* ---------- Zahlen lesbar machen ---------------------------------------- */
/** 1240 → "1.240". Tausenderpunkte, damit man vierstellige Zahlen
* auf einen Blick erfasst. */
const zahl = (n) => (n === null || n === undefined ? '–' : Number(n).toLocaleString('de-DE'));
/** Sekunden → "3:30 Min". Eine Verweildauer von 210 Sekunden liest
* niemand als dreieinhalb Minuten, wenn sie als "210" dasteht. */
function dauer(s) {
if (s === null || s === undefined) return '–';
const m = Math.floor(s / 60);
const rest = Math.round(s % 60);
return m ? `${m}:${String(rest).padStart(2, '0')} Min` : `${rest} Sek`;
}
/** Minuten → "1 Std 35". */
function stunden(min) {
if (min === null || min === undefined) return '–';
const h = Math.floor(min / 60);
const m = min % 60;
return h ? `${h} Std ${m}` : `${m} Min`;
}
/* ---------- Die Wochenkarten -------------------------------------------- */
const FELDER = [
/* Die Verweildauer steht VORN. Sie ist die Leitzahl (Recherche
06.09.2026), nicht eine von acht. */
{ key: 'verweildauer_s', name: 'Ø Verweildauer', form: dauer, leit: true },
{ key: 'diamanten', name: 'Diamanten', form: zahl },
{ key: 'gueltige_tage', name: 'LIVE-Tage', form: (n) => `${n} von 7` },
{ key: 'dauer_min', name: 'LIVE-Zeit', form: stunden },
{ key: 'zuschauer_avg', name: 'Ø Zuschauer', form: zahl },
{ key: 'schenker', name: 'Schenker', form: zahl },
{ key: 'follower_neu', name: 'Neue Follower', form: zahl },
];
function wochenKartenBauen() {
const ziel = $('zahlkarten');
ziel.textContent = '';
if (!stand) return;
for (const f of FELDER) {
const w = stand.woche[f.key];
if (!w) continue;
const karte = el('article', 'zahlkarte');
if (f.leit) karte.dataset.leit = 'ja';
karte.append(el('p', 'zahlkarte__name', f.name));
karte.append(el('p', 'zahlkarte__wert', f.form(w.wert)));
/* DER VERGLEICH. Ohne ihn ist die Zahl eine Behauptung. */
const v = el('p', 'zahlkarte__wandel');
if (w.wandel?.prozent !== undefined) {
const p = w.wandel.prozent;
v.dataset.richtung = p > 0 ? 'hoch' : p < 0 ? 'runter' : 'gleich';
v.textContent = `${p > 0 ? '+' : ''}${p} % gegenüber der Vorwoche`;
} else if (w.wandel?.neu) {
/* "Von null auf 500" -- keine Prozent, sondern die Auskunft,
die dahintersteckt. */
v.dataset.richtung = 'hoch';
v.textContent = 'neu – vorher nichts';
} else {
v.dataset.richtung = 'gleich';
v.textContent = w.vorher === null ? 'kein Vergleich' : 'wie in der Vorwoche';
}
karte.append(v);
/* DER VERLAUF als kleine Linie. Acht Wochen -- ohne Richtung ist
ein Wert nur ein Punkt. */
const linie = verlaufsLinie(stand.verlauf, f.key);
if (linie) karte.append(linie);
ziel.append(karte);
}
}
/** Eine kleine Linie aus den letzten acht Wochen.
*
* Selbst gezeichnet als SVG-Pfad, kein Diagrammpaket: Für acht
* Punkte eine Bibliothek zu laden wäre in einem Haus mit drei
* Abhängigkeiten die vierte -- und sie brächte Farben, Achsen und
* Beschriftungen mit, die hier alle stören würden. */
function verlaufsLinie(verlauf, key) {
if (!Array.isArray(verlauf) || verlauf.length < 3) return null;
/* == null FAENGT AUCH undefined -- und genau darauf kam es an.
Der Server liefert im Verlauf nur drei der sieben Groessen
(Diamanten, LIVE-Tage, Verweildauer); fuer alle anderen ist
w[key] undefined. Mit `=== null` rutschte das durch, wurde zu
Number(undefined) = NaN, und die Linie bekam Punkte wie
"NaN,NaN" -- der Browser zeichnet dann gar nichts und sagt
nichts. Eine leere Stelle in der Karte, ohne Fehlermeldung. */
const werte = verlauf.map((w) => {
const r = w?.[key];
if (r === null || r === undefined) return null;
const n = Number(r);
return Number.isFinite(n) ? n : null;
});
const echte = werte.filter((w) => w !== null);
if (echte.length < 3) return null;
const max = Math.max(...echte);
const min = Math.min(...echte);
const spanne = max - min || 1;
const B = 100, H = 26;
const punkte = werte.map((w, i) => {
if (w === null) return null;
const x = (i / (werte.length - 1)) * B;
/* Von unten nach oben: In SVG wächst y nach unten. */
const y = H - ((w - min) / spanne) * (H - 4) - 2;
return `${x.toFixed(1)},${y.toFixed(1)}`;
}).filter(Boolean);
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('class', 'zahlkarte__linie');
svg.setAttribute('viewBox', `0 0 ${B} ${H}`);
svg.setAttribute('preserveAspectRatio', 'none');
svg.setAttribute('aria-hidden', 'true');
svg.innerHTML = `<polyline points="${punkte.join(' ')}" />`;
/* Der letzte Punkt bekommt einen Kreis -- er ist das Jetzt. */
const letzter = punkte[punkte.length - 1].split(',');
svg.innerHTML += `<circle cx="${letzter[0]}" cy="${letzter[1]}" r="2.2" />`;
return svg;
}
/* ---------- Ziele als Fortschritt --------------------------------------- */
function zieleBauen() {
const ziel = $('zielbalken');
ziel.textContent = '';
const z = stand?.ziele;
$('ziele-aendern').hidden = !darfEintragen();
if (!z || (!z.diamanten && !z.tage && !z.verweildauer)) {
ziel.append(el('p', 'leistung__leer',
darfEintragen()
? 'Noch keine Ziele gesetzt. Aus „mehr Reichweite" wird hier eine prüfbare Größe.'
: 'Noch keine Ziele gesetzt.'));
return;
}
for (const [key, name, form] of [
['diamanten', 'Diamanten', zahl],
['tage', 'LIVE-Tage', (n) => String(n)],
['verweildauer', 'Ø Verweildauer', dauer],
]) {
const t = z[key];
if (!t) continue;
const zeile = el('div', 'zielbalken__zeile');
const kopf = el('div', 'zielbalken__kopf');
kopf.append(el('span', 'zielbalken__name', name));
kopf.append(el('span', 'zielbalken__zahl', `${form(t.ist)} von ${form(t.ziel)}`));
zeile.append(kopf);
const spur = el('div', 'zielbalken__spur');
const fuell = el('div', 'zielbalken__fuell');
/* Über 100 % wird nicht breiter -- der Balken bliebe sonst aus
dem Kasten hängen. Die Zahl daneben sagt es trotzdem. */
fuell.style.width = `${Math.min(100, Math.max(0, t.anteil))}%`;
if (t.anteil >= 100) fuell.dataset.voll = 'ja';
spur.append(fuell);
zeile.append(spur);
zeile.append(el('p', 'zielbalken__anteil',
t.anteil >= 100 ? `Ziel erreicht (${t.anteil} %)` : `${t.anteil} %`));
ziel.append(zeile);
}
}
const darfEintragen = () => LEITUNG.has(ich?.rolle) || ich?.rolle === 'scout';
/* ---------- Die Tageszeilen --------------------------------------------- */
async function tageLaden() {
let tage = [];
try {
const a = await hole(`/workspace/api/leistung/${offenerCreator}/tage?tage=21`);
if (a.ok) tage = (await a.json()).tage || [];
} catch { return; }
const ziel = $('tagetabelle');
ziel.textContent = '';
const nach = new Map(tage.map((t) => [t.tag, t]));
/* DIE LETZTEN 14 TAGE, auch die ohne Eintrag. Nur die vorhandenen
zu zeigen hieße, dass eine Lücke unsichtbar ist -- und genau die
Lücke ist die Auskunft: Da war nichts, oder es wurde nicht
erfasst. */
for (let i = 0; i < 14; i++) {
const d = new Date();
d.setDate(d.getDate() - i);
const p = (n) => String(n).padStart(2, '0');
const tag = `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
const z = nach.get(tag);
const zeile = el('div', 'tagzeile');
if (!z) zeile.dataset.leer = 'ja';
if (z?.quelle === 'import') zeile.dataset.quelle = 'import';
const datum = el('div', 'tagzeile__datum');
datum.append(el('span', 'tagzeile__wochentag',
d.toLocaleDateString('de-DE', { weekday: 'short' })));
datum.append(el('span', 'tagzeile__zahl',
d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' })));
zeile.append(datum);
const werte = el('div', 'tagzeile__werte');
if (z) {
werte.append(wert('Diamanten', zahl(z.diamanten)));
werte.append(wert('LIVE', stunden(z.dauer_min)));
werte.append(wert('Verweil', dauer(z.verweildauer_s)));
werte.append(wert('Ø Zusch.', zahl(z.zuschauer_avg)));
} else {
werte.append(el('span', 'tagzeile__leer', 'nichts erfasst'));
}
zeile.append(werte);
if (z?.notiz) {
const n = el('p', 'tagzeile__notiz', z.notiz);
zeile.append(n);
}
if (darfEintragen()) {
const knopf = el('button', 'tagzeile__knopf', z ? 'ändern' : 'eintragen');
knopf.type = 'button';
knopf.setAttribute('aria-label',
`${tag} ${z ? 'ändern' : 'eintragen'}`);
knopf.addEventListener('click', () => tagOeffnen(tag, z));
zeile.append(knopf);
}
ziel.append(zeile);
}
/* ZUGEKLAPPT AUF SIEBEN TAGE (07.09.2026, Wunsch Filipe).
Vierzehn Zeilen sind zu viel fuers Hinsehen und zu wenig fuers
Nachschlagen -- man scrollt an ihnen vorbei, statt sie zu lesen.
Sieben ist die Woche: der kuerzeste Zeitraum, ueber den man
ueberhaupt etwas sagen kann, und genau der, mit dem der
Wochenbericht rechnet. Die restlichen sieben Tage sind einen
Klick entfernt und verschwinden nicht.
Die Sieben steht nicht hier, sondern als eigene Regel im CSS
(`[data-klapp="zu7"]`) -- der Schalter kennt beide Stufen und
benennt sie im Knopftext, damit niemand raten muss, wie viel
gerade verborgen ist. */
window.klappe?.(ziel, document.getElementById('tage-schalter'),
document.getElementById('tage-schalter-text'), 7);
}
function wert(name, text) {
const s = el('span', 'tagzeile__wert');
s.append(el('span', 'tagzeile__wertname', name));
s.append(el('span', 'tagzeile__wertzahl', text));
return s;
}
/* ---------- Eintragen ---------------------------------------------------- */
function tagOeffnen(tag, z) {
const d = $('tag-dialog');
d.dataset.tag = tag;
const datum = new Date(tag + 'T12:00:00');
$('tag-titel').textContent = datum.toLocaleDateString('de-DE',
{ weekday: 'long', day: 'numeric', month: 'long' });
$('tag-unter').textContent = z?.quelle === 'import'
? 'Diese Zeile kam aus einer Datei. Änderungen von Hand bleiben erhalten, bis erneut eingelesen wird.'
: 'Leer lassen heißt: nicht gemessen. Eine Null ist etwas anderes.';
$('f-diamanten').value = z?.diamanten ?? '';
$('f-dauer').value = z?.dauer_min ?? '';
$('f-verweildauer').value = z?.verweildauer_s ?? '';
$('f-zavg').value = z?.zuschauer_avg ?? '';
$('f-zmax').value = z?.zuschauer_max ?? '';
$('f-schenker').value = z?.schenker ?? '';
$('f-follower').value = z?.follower_neu ?? '';
$('f-notiz').value = z?.notiz ?? '';
$('tag-fehler').textContent = '';
$('tag-loeschen').hidden = !z;
d.showModal();
$('f-diamanten').focus();
}
async function tagSpeichern(e) {
e.preventDefault();
const d = $('tag-dialog');
const tag = d.dataset.tag;
const n = (id) => {
const v = $(id).value.trim();
return v === '' ? null : Number(v);
};
const dauerMin = n('f-dauer');
try {
const a = await hole(`/workspace/api/leistung/${offenerCreator}/${tag}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
diamanten: n('f-diamanten'),
dauer_min: dauerMin,
/* Ein gültiger Tag ist ein Tag mit LIVE-Zeit -- daraus
geschlossen, statt ein weiteres Kästchen zu verlangen. */
gueltiger_tag: dauerMin > 0,
verweildauer_s: n('f-verweildauer'),
zuschauer_avg: n('f-zavg'),
zuschauer_max: n('f-zmax'),
schenker: n('f-schenker'),
follower_neu: n('f-follower'),
notiz: $('f-notiz').value,
}),
});
if (!a.ok) {
$('tag-fehler').textContent = (await a.json().catch(() => ({}))).fehler || 'Ging nicht.';
return;
}
d.close();
await allesLaden();
} catch { /* umgeleitet */ }
}
async function tagLoeschen() {
const d = $('tag-dialog');
if (!window.confirm('Diese Zeile wirklich löschen? Die Zahlen des Tages sind dann weg.')) return;
try {
const a = await hole(`/workspace/api/leistung/${offenerCreator}/${d.dataset.tag}`,
{ method: 'DELETE' });
if (!a.ok) { $('tag-fehler').textContent = 'Ging nicht.'; return; }
d.close();
await allesLaden();
} catch { /* umgeleitet */ }
}
/* ---------- Ziele ------------------------------------------------------- */
function zieleOeffnen() {
const z = stand?.ziele;
$('z-diamanten').value = z?.diamanten?.ziel ?? '';
$('z-tage').value = z?.tage?.ziel ?? '';
$('z-verweildauer').value = z?.verweildauer?.ziel ?? '';
$('ziele-fehler').textContent = '';
$('ziele-dialog').showModal();
}
async function zieleSpeichern(e) {
e.preventDefault();
const n = (id) => {
const v = $(id).value.trim();
return v === '' ? null : Number(v);
};
try {
const a = await hole(`/workspace/api/leistung/${offenerCreator}/ziele`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
diamanten_woche: n('z-diamanten'),
tage_woche: n('z-tage'),
verweildauer_s: n('z-verweildauer'),
}),
});
if (!a.ok) { $('ziele-fehler').textContent = 'Ging nicht.'; return; }
$('ziele-dialog').close();
await allesLaden();
} catch { /* umgeleitet */ }
}
/* ---------- Import ------------------------------------------------------ */
async function dateiGewaehlt(e) {
const datei = e.target.files?.[0];
if (!datei) return;
$('import-fehler').textContent = '';
$('vorschau').hidden = true;
$('import-los').disabled = true;
try {
importText = await datei.text();
} catch {
$('import-fehler').textContent = 'Die Datei ließ sich nicht lesen.';
return;
}
/* ERST DIE VORSCHAU. Ein Import, der sofort schreibt, ist bei
einer falsch zugeordneten Spalte nicht mehr zurückzuholen. */
try {
const a = await hole(`/workspace/api/leistung/${offenerCreator}/import/vorschau`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: importText }),
});
const v = await a.json();
if (!a.ok) {
$('import-fehler').textContent = v.fehler || 'Die Datei konnte nicht gelesen werden.';
return;
}
importSpalten = v.spalten;
vorschauZeigen(v);
} catch { /* umgeleitet */ }
}
function vorschauZeigen(v) {
const ziel = $('vorschau');
ziel.textContent = '';
ziel.hidden = false;
/* Die ZAHLEN stehen vorn: Wer übernimmt, will wissen "wie viele",
nicht "welche". */
const zeile = el('div', 'import-zahlen');
zeile.append(importZahl(v.neu, 'neu'));
zeile.append(importZahl(v.ersetzt, v.ersetzt === 1 ? 'wird ersetzt' : 'werden ersetzt'));
if (v.fehlerhaft) zeile.append(importZahl(v.fehlerhaft, 'unlesbar', true));
ziel.append(zeile);
/* Welche Spalten erkannt wurden -- damit man merkt, wenn etwas
fehlt. Eine stillschweigend nicht erkannte Spalte wäre eine
Zahl, die einfach nie ankommt. */
const erkannt = Object.keys(importSpalten || {}).filter((k) => k !== 'tag');
const namen = {
diamanten: 'Diamanten', dauer_min: 'LIVE-Dauer', zuschauer_avg: 'Ø Zuschauer',
zuschauer_max: 'Spitze', verweildauer_s: 'Verweildauer',
schenker: 'Schenker', follower_neu: 'Follower',
};
ziel.append(el('p', 'import-spalten',
erkannt.length
? 'Erkannt: ' + erkannt.map((k) => namen[k] || k).join(' · ')
: 'Außer dem Datum wurde keine Spalte erkannt – die Zahlen bleiben leer.'));
if (v.probleme?.length) {
const liste = el('ul', 'import-probleme');
for (const p of v.probleme) {
liste.append(el('li', null, `Zeile ${p.zeile}: ${p.grund}`));
}
ziel.append(liste);
}
$('import-los').disabled = !(v.neu + v.ersetzt);
}
function importZahl(n, name, warn = false) {
const k = el('div', 'import-zahl');
if (warn) k.dataset.warn = 'ja';
k.append(el('span', 'import-zahl__wert', String(n)));
k.append(el('span', 'import-zahl__name', name));
return k;
}
async function importUebernehmen(e) {
e.preventDefault();
if (!importText) return;
const knopf = $('import-los');
knopf.disabled = true;
try {
const a = await hole(`/workspace/api/leistung/${offenerCreator}/import`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: importText, spalten: importSpalten }),
});
const r = await a.json();
if (!a.ok) { $('import-fehler').textContent = r.fehler || 'Ging nicht.'; return; }
$('import-dialog').close();
$('datei').value = '';
importText = '';
await allesLaden();
melde(`${r.geschrieben} Tage übernommen.`);
setTimeout(() => melde(''), 5000);
} catch { /* umgeleitet */ } finally { knopf.disabled = false; }
}
/* ---------- Laden ------------------------------------------------------- */
async function allesLaden() {
if (!offenerCreator) return;
try {
const a = await hole(`/workspace/api/leistung/${offenerCreator}`);
if (!a.ok) { melde('Die Zahlen konnten nicht geladen werden.'); return; }
stand = await a.json();
} catch { return; }
$('woche').hidden = false;
$('ziele-block').hidden = false;
$('tage-block').hidden = false;
/* WIE VOLLSTÄNDIG IST DIE WOCHE? Eine Zahl aus zwei von sieben
Tagen ist etwas anderes als eine aus sieben -- ohne diesen
Hinweis hält man eine halbe Woche für einen Einbruch. */
const e = stand.erfassung;
$('erfassung').textContent = e.tage >= 7
? 'alle 7 Tage erfasst'
: `${e.tage} von 7 Tagen erfasst`;
$('erfassung').dataset.knapp = e.tage < 4 ? 'ja' : '';
wochenKartenBauen();
zieleBauen();
await tageLaden();
}
/* ---------- Start -------------------------------------------------------- */
$('tag-form').addEventListener('submit', tagSpeichern);
$('tag-zu').addEventListener('click', () => $('tag-dialog').close());
$('tag-loeschen').addEventListener('click', tagLoeschen);
$('ziele-aendern').addEventListener('click', zieleOeffnen);
$('ziele-form').addEventListener('submit', zieleSpeichern);
$('ziele-zu').addEventListener('click', () => $('ziele-dialog').close());
$('import-oeffnen').addEventListener('click', () => {
$('vorschau').hidden = true;
$('import-los').disabled = true;
$('import-fehler').textContent = '';
$('import-dialog').showModal();
});
$('import-zu').addEventListener('click', () => $('import-dialog').close());
$('datei').addEventListener('change', dateiGewaehlt);
$('import-form').addEventListener('submit', importUebernehmen);
(async () => {
try {
const a = await hole('/workspace/api/ich');
if (!a.ok) { location.assign('/workspace/'); return; }
ich = await a.json();
} catch { return; }
window.werZeigen?.(ich);
/* Ein Creator sieht nur sich selbst -- ohne Auswahlfeld, das nur
einen Eintrag hätte. */
if (ich.rolle === 'creator') {
offenerCreator = ich.id;
await allesLaden();
return;
}
try {
const { creator: liste } = await (await hole('/workspace/api/leistung')).json();
creator = liste || [];
} catch { return; }
if (!creator.length) {
melde('Dir ist noch kein Creator zugeordnet.');
return;
}
$('creator-wahl').hidden = false;
for (const c of creator) {
const o = document.createElement('option');
o.value = String(c.id);
/* Die wichtigste Zahl steht gleich in der Auswahl -- so sieht
man beim Umschalten, wo etwas los ist. */
o.textContent = c.tage_erfasst
? `${c.name} — ${zahl(c.diamanten)} Diamanten`
: `${c.name} — noch keine Zahlen`;
$('creator').append(o);
}
/* Vom Dashboard kommend: direkt zur richtigen Person. */
const wunsch = Number(new URLSearchParams(location.search).get('creator'));
offenerCreator = creator.some((c) => c.id === wunsch) ? wunsch : creator[0].id;
$('creator').value = String(offenerCreator);
window.personenwahl?.('creator');
$('creator').addEventListener('change', async () => {
offenerCreator = Number($('creator').value);
history.replaceState(null, '', `leistung.html?creator=${offenerCreator}`);
await allesLaden();
});
await allesLaden();
})();
})();