Files
dogfather-universe/workspace/assets/js/leistung.js
T
DogFatherGitandClaude Opus 5 8fc1ac7a7a Alles aus der Excel-Datei -- und der Fund, der alles blockiert haette
Filipe: "ich will dass alles von der excel datei genommen wird.
perfektionnier das, aber wenn ich dir runter lade soll alles notiert
und angezeigt werden." Dazu eine echte Ausgabe als Vorlage.

DER SCHWERSTE FUND STECKTE VOR DEN DATEN, NICHT IN IHNEN.

Seine Ausgabe "Creator_innendaten" hat DREI Spalten, die nach Person
aussehen: "Creator*in-ID", "Creator*innen-Anmeldename" und "Agent".
`personSpaltenRaten` nahm die erste mit dem Wort "creator" darin --
die ID. Danach wurde nach einem Creator namens "700001" gesucht.
Nachgemessen an seiner echten Kopfzeile: handle = KEINE, name =
"Creator*in-ID". Diese Datei haette KEINE EINZIGE Zeile zugeordnet,
mit einem Hinweis ("steht bei keinem Creator im Feld TikTok"), der in
die voellig falsche Richtung zeigt.

Jetzt ist "Anmeldename" der Handle, eine Kennnummer ist fuer beide
Spalten ausgeschlossen, und "creator" allein reicht nicht mehr als
Namensspalte -- sonst haette weiter hinten "Neue*r LIVE-Creator*innen"
(Wert: "Nein") die Stelle uebernommen. An fuenf Kopfzeilen gemessen.

UND DANN: NICHTS FAELLT MEHR WEG.

41 Spalten in der Datei, acht werden gedeutet. Die restlichen 33 --
letzter Monat, fuenf Prozentwerte, Matches, Multi-Gast-LIVEs, Fanclub,
Graduierungs- und Stufenstatus -- wurden lautlos weggeworfen.

KEINE 33 NEUEN SPALTEN, sondern eine Zeile je Spalte mit dem NAMEN als
Schluessel. Eine abgeschriebene Spaltenliste hat in diesem Haus schon
zweimal Daten gekostet und waere beim naechsten Backstage-Update
falsch. Gegenprobe in der Pruefung: eine erfundene Spalte
("Sternenstaub pro Woche") kommt genauso durch -- es wird also keine
Liste gepflegt, die Datei entscheidet.

An SEINER echten Datei gemessen, ohne sie irgendwo hineinzuschreiben:
40 von 41 Spalten gespeichert (die 41. ist leer), Zeitraum 01.09.-
13.09. erkannt, 32 als Zahl, 8 als Text.

UND EIN MESSFEHLER, DER LEHRREICH IST: Meine erste Pruefung meldete
"zugeklappt ist die Liste 141 px hoch", im Bildschirmfoto war dort
nichts. An einem Miniaturfall nachgemessen: getBoundingClientRect,
offsetHeight, offsetParent und getClientRects liefern bei einem
<details> in BEIDEN Zustaenden identische Werte -- Chromium verbirgt
den Inhalt mit content-visibility:hidden, und das behaelt die letzte
Ausmessung. Nur checkVisibility() kann es unterscheiden. Die Messung
log, nicht die Seite.

pruef-backstage-import 160 (war 133), pruef-xlsx 75,
pruef-leistung-optik 59, pruef-leistung, pruef-css-klassen und
pruef-auskunft (46, DSGVO -- die neue Tabelle ist automatisch dabei,
weil die Auskunft ihre Liste aus PRAGMA foreign_key_list ableitet):
alle gruen.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-16 01:46:07 +02:00

1107 lines
45 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;
};
/* Die Leitungsliste steht in bereiche.js -- eine Antwort statt neun.
Sie stand hier frueher selbst, ohne 'spicy', und genau das war der
Fehler, den Filipe am 07.09.2026 gefunden hat. */
const LEITUNG = window.Bereiche.LEITUNG;
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();
/* ALLE EINTRAG-KNOEPFE AN EINER STELLE (11.09.2026).
Seit heute oeffnet sich diese Seite fuer jede Rolle -- Filipe:
"jetzt soll jede rolle diese kachel sehen ... aber die creator
kriegen dan quasi nur ihre daten zu sehen."
GEMESSEN, NICHT ANGENOMMEN: Beim Umbau fiel auf, dass
'import-oeffnen' als einziger Knopf NIE eine Rechteregel hatte.
Das war folgenlos, solange nur DogFather hereinkam. Ab heute
haette ein Creator dort "Aus Datei einlesen" gesehen und eine
Absage bekommen. Genau deshalb steht die Liste jetzt hier: Wer
einen Knopf hinzufuegt, sieht die Regel daneben und kann sie
nicht vergessen. */
const KNOEPFE = [
/* Eigene Datei mit Tageszahlen -- wer eintragen darf, darf das. */
['import-oeffnen', darfEintragen()],
/* Der Backstage-Weg gilt fuer ALLE Creator auf einmal -- deshalb
nur fuer die Leitung, nicht fuer einen Scout, der ohnehin nur
seine eigenen sieht. Der Server prueft dasselbe (netzDarf);
hier geht es darum, niemandem einen Knopf hinzustellen, der
eine Absage bringt. */
['netz-oeffnen', LEITUNG.has(ich?.rolle)],
];
for (const [knopf, darf] of KNOEPFE) $(knopf).hidden = !darf;
/* Jeder Erklaersatz haengt an SEINEM Knopf, nicht am Block. Ein
Scout, der die Backstage-Tabelle nicht hat, soll sie auch nicht
erklaert bekommen -- das waere keine Hilfe, sondern eine Absage,
die er nicht gesucht hat. Und ein Creator sieht keinen Knopf,
also auch keinen Satz.
Die Liste hat seit dem 11.09.2026 nur noch einen Eintrag (der
Weg ueber die TikTok-Datei des Creators ist entfernt). Sie
bleibt trotzdem eine Liste: Der naechste Weg haengt sich dann
daneben, statt dass jemand die Regel ein zweites Mal
aufschreibt. */
for (const [satz, knopf] of [['quelle-netz', 'netz-oeffnen']]) {
$(satz).hidden = $(knopf).hidden;
}
$('quellen-hinweis').hidden = $('quelle-netz').hidden;
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;
/* WAS IST DAS UEBERHAUPT FUER EINE DATEI? (11.09.2026)
Filipe: "meine excel datei kann ich nicht auswaehlen wieso??"
Bis heute stand im Auswahlfeld nur `.csv` -- eine .xlsx war
ausgegraut, ohne ein Wort dazu. Eine Sackgasse ohne Wegweiser
ist schlimmer als eine Absage mit Begruendung: Man sucht den
Fehler bei sich.
Jetzt laesst sich jede Datei waehlen, und WAS sie ist, sagt der
INHALT -- nicht die Endung. Eine umbenannte Datei ist keine
andere Datei. Eine .xlsx ist in Wahrheit ein ZIP-Archiv und
beginnt mit "PK"; die alte .xls beginnt mit dem
OLE-Kennzeichen D0 CF 11 E0.
WARUM WIR SIE NICHT EINFACH LESEN: Der Leser dahinter versteht
Text (Komma, Semikolon, Tabulator). Ein ZIP-Archiv als Text
gelesen ergibt Zeichensalat -- und der schlimmste Ausgang waere
nicht "geht nicht", sondern eine Vorschau mit Zahlen, die
irgendwo aus dem Dateikopf stammen. */
/* WAS IST DAS UEBERHAUPT FUER EINE DATEI? (11.09.2026, erweitert 14.09.2026)
Filipe zuerst: "meine excel datei kann ich nicht auswaehlen
wieso??" -- dann: "mach doch bitte so dass man alle dateien hoch
laden könne auch excel dateien."
Erkannt wird am INHALT, nicht an der Endung: Eine umbenannte
Datei ist keine andere Datei. Eine .xlsx ist ein ZIP-Archiv und
beginnt mit "PK", die alte .xls mit dem OLE-Kennzeichen
D0 CF 11 E0.
Seit heute wird die .xlsx nicht mehr abgewiesen, sondern
GELESEN: Der Server macht daraus eine TAB-getrennte Tabelle --
genau die Form, die auch beim Einfuegen aus dem Browser
entsteht. Danach laeuft alles durch dieselbe Vorschau wie
bisher. */
const anfang = new Uint8Array(await datei.slice(0, 8).arrayBuffer());
const beginnt = (...b) => b.every((x, i) => anfang[i] === x);
let ausExcel = null;
if (beginnt(0x50, 0x4b)) {
$('import-fehler').textContent = 'Excel-Datei wird gelesen …';
try {
const a = await hole('/workspace/api/leistung/tabelle-lesen', {
method: 'POST',
headers: { 'Content-Type': 'application/octet-stream' },
body: await datei.arrayBuffer(),
});
const d = await a.json().catch(() => ({}));
if (!a.ok) {
$('import-fehler').textContent = d.fehler || 'Die Excel-Datei ließ sich nicht lesen.';
$('datei').value = '';
return;
}
importText = d.text;
ausExcel = d;
$('import-fehler').textContent = '';
} catch {
$('import-fehler').textContent = 'Die Excel-Datei konnte nicht gelesen werden.';
$('datei').value = '';
return;
}
} else if (beginnt(0xd0, 0xcf, 0x11, 0xe0)) {
/* Die ALTE .xls ist kein ZIP, sondern ein ganz anderes Format
(BIFF, vor 2007). Sie zu lesen waere ein zweiter Leser fuer
einen Fall, den Backstage nicht erzeugt. */
$('import-fehler').innerHTML =
'Das ist eine <strong>alte Excel-Datei (.xls)</strong>. Speichere sie in Excel '
+ 'einmal als <em>.xlsx</em> oder als <em>CSV UTF-8</em> – beides geht dann durch.';
$('datei').value = '';
return;
}
if (!ausExcel) {
try {
importText = await datei.text();
} catch {
$('import-fehler').textContent = 'Die Datei ließ sich nicht lesen.';
return;
}
}
/* Eine leere Datei ist kein Lesefehler, sieht aber wie einer aus. */
if (!importText.trim()) {
$('import-fehler').textContent = 'Die Datei ist leer.';
$('datei').value = '';
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();
/* WAS GENAU ANGEKOMMEN IST -- Tage und Zeitraeume getrennt.
"2 Tage uebernommen" waere bei einer Backstage-Ausgabe ueber
zwei Wochen schlicht falsch; man wuerde sie in der Tagesliste
suchen und nicht finden. */
const teile = [];
if (r.geschrieben) teile.push(`${r.geschrieben} Tag${r.geschrieben === 1 ? '' : 'e'}`);
if (r.zeitraeume) teile.push(`${r.zeitraeume} Zeitraum${r.zeitraeume === 1 ? '' : 'e'}`);
if (r.felder) teile.push(`${r.felder} Werte aus der Datei`);
melde(teile.length ? `${teile.join(' und ')} übernommen.` : 'Nichts zu übernehmen.');
setTimeout(() => melde(''), 5000);
} catch { /* umgeleitet */ } finally { knopf.disabled = false; }
}
/* ===================================================================
ZAHLEN FÜR EINEN ZEITRAUM (14.09.2026)
Filipe: "ich will dass das auch so geht, mach dass es
funktioniert ... sieh zu dass die excel auch so durch geht."
Backstage gibt eine Ausgabe für einen ZEITRAUM heraus -- alle
Zahlen aufsummiert, eine Zeile je Creator. Auf einen Tag
geschrieben wären das dreizehn Tage auf einmal: eine Zahl, die
richtig aussieht und falsch ist. Also stehen sie hier als das, was
sie sind.
DER BLOCK VERSCHWINDET, WENN ES NICHTS GIBT. Eine leere
Überschrift ist keine Ordnung, sondern eine Lücke.
=================================================================== */
function spannenZeichnen() {
const liste = stand?.zeitraeume || [];
$('spannen-block').hidden = liste.length === 0;
const ziel = $('spannen');
ziel.textContent = '';
if (!liste.length) return;
for (const z of liste) {
const karte = el('article', 'spanne');
/* ---- Kopf: die Spanne gross, die Dauer als Marke ------------- */
const kopf = el('header', 'spanne__kopf');
const spanne = el('div', 'spanne__spanne');
spanne.append(el('span', 'spanne__von', kurzTag(z.von)));
spanne.append(el('span', 'spanne__strich', '–'));
spanne.append(el('span', 'spanne__bis', kurzTag(z.bis)));
kopf.append(spanne);
/* DIE ZAHL DER TAGE IST DIE WICHTIGSTE EINORDNUNG: "13.210
Diamanten" ueber drei Tage ist etwas anderes als ueber
dreizehn. Sie steht deshalb neben der Spanne und nicht
irgendwo unten. */
kopf.append(el('span', 'spanne__tage', `${z.tage} Tage`));
karte.append(kopf);
/* ---- Die Hauptzahl -------------------------------------------- */
if (z.diamanten !== null && z.diamanten !== undefined) {
const haupt = el('div', 'spanne__haupt');
haupt.append(el('span', 'spanne__haupt-zahl', zahl(z.diamanten)));
haupt.append(el('span', 'spanne__haupt-name', 'Diamanten'));
if (z.tage > 0) {
/* DIE EINZIGE UMRECHNUNG, DIE EHRLICH IST -- und sie steht
als Durchschnitt da, nicht als Tageswert. Gespeichert wird
sie nirgends. */
haupt.append(el('span', 'spanne__schnitt',
`Ø ${zahl(Math.round(z.diamanten / z.tage))} pro Tag`));
}
karte.append(haupt);
}
/* ---- Wie viele Tage davon wirklich live waren (16.09.2026) -----
Backstage liefert "Gültige LIVE-Gehen-Tage" mit. Die Zahl stand
bisher als eine von fünf gleich aussehenden unten im Raster --
dabei ist sie die EINZIGE, die sagt, wie die Diamantenzahl
zustande kam: 13.210 an elf Tagen ist etwas anderes als
13.210 an dreien.
EIN STREIFEN STATT EINES BALKENS. Er zeigt einen Anteil, keinen
Fortschritt -- deshalb keine Bewegung, kein Füllen, kein Ziel.
GERECHNET WIRD NUR, WENN BEIDE ZAHLEN DA SIND. Ohne die Zahl
der Tage wäre der Anteil eine Division durch nichts, und ein
Streifen ohne Bezug zeigt eine Genauigkeit, die es nicht gibt. */
if (z.gueltige_tage !== null && z.gueltige_tage !== undefined && z.tage > 0) {
/* NICHT GERUNDET. 11 von 13 sind 84,615 %; auf 85 gerundet
ragte die Füllung vier Pixel in den zwölften Tag hinein --
auf einem in Tage geteilten Streifen sieht das aus wie ein
angefangener Tag, den es nicht gibt. */
const anteil = Math.max(0, Math.min(100, (z.gueltige_tage / z.tage) * 100));
const lauf = el('div', 'spanne__lauf');
const bahn = el('div', 'spanne__lauf-bahn');
const teil = el('div', 'spanne__lauf-teil');
teil.style.width = anteil + '%';
/* Die Zahl der Tage geht als Wert an den Stil -- er teilt den
Streifen daraufhin in genau so viele Kästchen. Ab 32 Tagen
wäre jede Zelle schmaler als ihre Fuge, dann bleibt es bei
der glatten Bahn. Die Grenze steht im Stil erklärt. */
bahn.style.setProperty('--tage', String(z.tage));
if (z.tage <= 31) bahn.dataset.teilen = 'ja';
bahn.append(teil);
lauf.append(bahn);
lauf.append(el('span', 'spanne__lauf-text',
`${z.gueltige_tage} von ${z.tage} Tagen live`));
karte.append(lauf);
}
/* ---- Die uebrigen Werte --------------------------------------- */
const werte = el('div', 'spanne__werte');
const zeig = (name, wert, form) => {
if (wert === null || wert === undefined) return;
const w = el('div', 'spanne__wert');
w.append(el('span', 'spanne__name', name));
w.append(el('span', 'spanne__zahl', form(wert)));
werte.append(w);
};
zeig('LIVE-Zeit', z.dauer_min, stunden);
/* "Gültige Tage" steht NICHT mehr hier -- es steht als Streifen
darüber. Zweimal dieselbe Zahl wäre zweimal dieselbe Auskunft
mit zwei verschiedenen Rängen. */
zeig('Ø Zuschauer', z.zuschauer_avg, zahl);
zeig('Spitze', z.zuschauer_max, zahl);
zeig('Neue Follower', z.follower_neu, zahl);
if (werte.children.length) karte.append(werte);
/* ---- UND ALLES ANDERE, WAS IN DER DATEI STAND (16.09.2026) ----
Filipe: "ich will dass alles von der excel datei genommen wird.
... wenn ich dir runter lade soll alles notiert und angezeigt
werden."
Seine Ausgabe hat 41 Spalten; oben stehen die sechs, die das
System deutet. Der Rest -- letzter Monat, fünf Prozentwerte,
Matches, Multi-Gast-LIVEs, Fanclub, Graduierungs- und
Stufenstatus -- steht hier, WÖRTLICH wie in der Datei.
IN DER REIHENFOLGE DER DATEI, nicht in einer eigenen: Backstage
gruppiert seine Spalten schon sinnvoll, und jede Ordnung, die
ich erfinde, ist beim nächsten Backstage-Update falsch.
ZUGEKLAPPT, ABER MIT DER ZAHL IM DECKEL. Achtunddreißig Zeilen
offen unter jedem Zeitraum wären die Karte nicht mehr lesbar --
und genau die Lesbarkeit war gestern sein Thema. Im Deckel
steht, wie viele es sind; damit sieht man ohne Öffnen, dass
nichts fehlt. */
if (Array.isArray(z.felder) && z.felder.length) {
const alles = el('details', 'spanne__alles');
alles.append(el('summary', 'spanne__alles-kopf',
`Alle ${z.felder.length} Werte aus Backstage`));
const liste = el('dl', 'spanne__alles-liste');
for (const f of z.felder) {
liste.append(el('dt', 'spanne__alles-name', f.name));
liste.append(el('dd', 'spanne__alles-wert', f.text));
}
alles.append(liste);
karte.append(alles);
}
ziel.append(karte);
}
}
/** "2026-09-01" -> "01.09." -- in einer Spanne zählt der Tag, nicht
* das Jahr; beide Enden im selben Jahr wären doppelt genannt. */
function kurzTag(iso) {
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(iso || ''));
return m ? `${m[3]}.${m[2]}.` : String(iso || '');
}
/* ---------- 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;
spannenZeichnen();
/* 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' : '';
/* KEIN TAG ERFASST -> DIE ANTWORT STATT SIEBEN NULLEN (14.09.2026).
Filipe: "wieso werden die zahlen oben nicht ausgefüllt von der
excel datei?"
Die Frage entsteht an DIESER Stelle, also steht die Antwort hier.
Sieben Karten mit "0" und "wie in der Vorwoche" sehen aus wie ein
Fehler -- dabei ist einfach kein einziger Tag erfasst, weil die
Backstage-Ausgabe einen ZEITRAUM enthielt.
Und sie sagt nicht nur WARUM, sondern auch, was zu tun ist:
in Backstage den Zeitraum auf einen Tag stellen. Ein Hinweis ohne
Ausweg ist eine Sackgasse mit Erklärung. */
const spannen = stand?.zeitraeume || [];
const leer = $('woche-leer');
if (e.tage === 0) {
leer.textContent = '';
leer.append(el('p', 'woche-leer__satz',
'Für diese Woche ist noch kein einziger Tag erfasst.'));
if (spannen.length) {
const s0 = spannen[0];
leer.append(el('p', 'woche-leer__grund',
`Deine Backstage-Ausgabe deckt ${kurzTag(s0.von)} – ${kurzTag(s0.bis)} ab `
+ `(${s0.tage} Tage auf einmal). Aus einer Summe über ${s0.tage} Tage lässt `
+ 'sich nicht ablesen, wie ein einzelner Tag lief – deshalb steht sie unten '
+ 'als Zeitraum und nicht hier oben.'));
leer.append(el('p', 'woche-leer__weg',
'Wenn du die Wochenzahlen füllen willst: In Backstage den Zeitraum auf '
+ 'EINEN Tag stellen, herunterladen, hier einlesen – je Tag einmal.'));
} else {
leer.append(el('p', 'woche-leer__grund',
'Trag einen Tag von Hand ein oder lies eine Datei mit Tageszeilen ein.'));
}
leer.hidden = false;
$('zahlkarten').hidden = true;
} else {
leer.hidden = true;
$('zahlkarten').hidden = false;
}
wochenKartenBauen();
zieleBauen();
await tageLaden();
}
/* ---------- Start -------------------------------------------------------- */
$('tag-form').addEventListener('submit', tagSpeichern);
$('tag-zu').addEventListener('click', () => $('tag-dialog').close());
$('tag-loeschen').addEventListener('click', tagLoeschen);
/* =====================================================================
DIE BACKSTAGE-TABELLE — Vorschau und Übernahme
===================================================================== */
async function netzVorschau() {
const text = $('netz-text').value.trim();
$('netz-fehler').textContent = '';
if (!text) { $('netz-vorschau').hidden = true; $('netz-los').disabled = true; return; }
const a = await hole('/workspace/api/leistung/netzwerk-import/vorschau', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, tag: $('netz-tag').value || null }),
}).catch(() => null);
if (!a) return;
const d = await a.json().catch(() => ({}));
if (!a.ok) {
$('netz-vorschau').hidden = true;
$('netz-los').disabled = true;
$('netz-fehler').textContent = d.fehler || 'Die Tabelle konnte nicht gelesen werden.';
/* Fehlt nur der Tag, ist das kein Fehler, sondern eine Frage --
also wird das Feld hervorgehoben statt nur gemeckert. */
if (d.braucht_tag) $('netz-tag').focus();
return;
}
const v = $('netz-vorschau');
v.textContent = '';
v.hidden = false;
const zeile = (text2, art) => {
const p = document.createElement('p');
p.className = 'import-vorschau__zeile';
if (art) p.dataset.art = art;
p.textContent = text2;
v.append(p);
};
zeile(`${d.zugeordnet} von ${d.zugeordnet + d.unbekannt.length} Zeilen zugeordnet`
+ ` · ${d.neu} neu, ${d.ersetzt} werden ersetzt`, d.zugeordnet ? 'gut' : 'warn');
zeile(`Erkannte Spalten: ${Object.keys(d.spalten).join(', ') || 'keine'}`
+ (d.aus_tabelle ? ' · Datum aus der Tabelle' : ' · Datum aus dem Feld oben'));
/* WAS MIT DEM REST PASSIERT -- und das ist seit dem 16.09. die
Zeile, auf die es Filipe ankommt: "ich will dass alles von der
excel datei genommen wird."
Die Zeile darueber nennt nur die ACHT Spalten, die das System
deutet. Seine echte Ausgabe hat 41. Ohne diese Zeile sieht es
aus, als fielen 33 davon weg -- sie werden aber vollstaendig
mitgeschrieben und stehen danach auf der Karte. */
if (d.spalten_gesamt) {
zeile(`Alle ${d.spalten_gesamt} Spalten der Datei werden übernommen`
+ (d.felder ? ` · ${d.felder} davon tragen einen Wert` : '')
+ ' · sie stehen danach unter „Alle Werte aus Backstage“', 'gut');
}
/* DIE UNBEKANNTEN SIND DER WICHTIGE TEIL. Wer sie übersieht, hat
stillschweigend Leute ausgelassen -- und merkt es erst, wenn die
Zahlen nicht stimmen. Deshalb stehen sie mit Namen da und nicht
als Zahl. */
if (d.unbekannt.length) {
zeile(`Nicht zugeordnet (${d.unbekannt.length}): ${d.unbekannt.slice(0, 8).join(', ')}`
+ (d.unbekannt.length > 8 ? ' …' : ''), 'warn');
zeile('Diese Namen stehen bei keinem Creator im Feld „TikTok" im Steckbrief. '
+ 'Dort eintragen – danach werden sie beim nächsten Mal gefunden.', 'still');
}
if (d.probleme_gesamt) {
zeile(`${d.probleme_gesamt} Zeile(n) übersprungen:`, 'warn');
for (const p of d.probleme) zeile(`Zeile ${p.zeile}: ${p.grund}`, 'still');
}
for (const b of d.beispiele) {
const zahlen = Object.entries(b)
/* `felder` ist eine LISTE und gehoert nicht in diese Zeile --
ohne diesen Ausschluss stand hier woertlich
"felder [object Object],[object Object]". */
.filter(([k]) => !['creator_id', 'name', 'tag', 'felder'].includes(k))
.filter(([, w]) => w !== null && w !== undefined)
.map(([k, w]) => `${k} ${w}`).join(' · ');
zeile(`${b.name} · ${b.tag} · ${zahlen}`, 'still');
}
$('netz-los').disabled = d.zugeordnet === 0;
}
async function netzUebernehmen(ereignis) {
ereignis.preventDefault();
$('netz-los').disabled = true;
const a = await hole('/workspace/api/leistung/netzwerk-import', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: $('netz-text').value, tag: $('netz-tag').value || null,
}),
}).catch(() => null);
if (!a) { $('netz-los').disabled = false; return; }
const d = await a.json().catch(() => ({}));
if (!a.ok) {
$('netz-fehler').textContent = d.fehler || 'Übernehmen ging nicht.';
$('netz-los').disabled = false;
return;
}
$('netz-dialog').close();
melde(`${d.geschrieben} Zeile(n) übernommen`
+ (d.unbekannt?.length ? ` · ${d.unbekannt.length} nicht zugeordnet` : ''));
await laden();
}
$('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);
/* ---------- Die Backstage-Tabelle ------------------------------------ */
$('netz-oeffnen').addEventListener('click', () => {
$('netz-vorschau').hidden = true;
$('netz-los').disabled = true;
$('netz-fehler').textContent = '';
$('netz-text').value = '';
$('netz-dialog').showModal();
$('netz-text').focus();
});
$('netz-zu').addEventListener('click', () => $('netz-dialog').close());
/* DIE BACKSTAGE-AUSGABE ALS DATEI (14.09.2026).
Sie enthaelt ALLE Creator auf einmal -- deshalb hier und nicht im
Einzel-Import daneben. Der Weg ist bewusst derselbe wie beim
Einfuegen: Die Datei wird zu Text, der Text landet im Feld, und ab
da laeuft die vorhandene Vorschau. Ein eigener Ablauf fuer Dateien
waere eine zweite Fassung derselben Regeln. */
$('netz-datei').addEventListener('change', async (e) => {
const datei = e.target.files?.[0];
if (!datei) return;
$('netz-fehler').textContent = 'Datei wird gelesen …';
try {
const roh = new Uint8Array(await datei.slice(0, 8).arrayBuffer());
let text = null;
let ausExcel = null;
if (roh[0] === 0x50 && roh[1] === 0x4b) {
const a = await hole('/workspace/api/leistung/tabelle-lesen', {
method: 'POST',
headers: { 'Content-Type': 'application/octet-stream' },
body: await datei.arrayBuffer(),
});
const d = await a.json().catch(() => ({}));
if (!a.ok) { $('netz-fehler').textContent = d.fehler || 'Ging nicht.'; return; }
text = d.text;
ausExcel = d;
} else if (roh[0] === 0xd0 && roh[1] === 0xcf) {
$('netz-fehler').textContent = 'Das ist eine alte Excel-Datei (.xls). '
+ 'Speichere sie einmal als .xlsx oder als CSV.';
return;
} else {
text = await datei.text();
}
if (!text || !text.trim()) { $('netz-fehler').textContent = 'Die Datei ist leer.'; return; }
$('netz-text').value = text;
$('netz-fehler').textContent = '';
/* WAS ICH MIT DER DATEI GEMACHT HABE, STEHT DA. Eine Tabelle, die
stillschweigend anders anfaengt als die Datei (Backstage setzt
bei einer Ausgabe eine Titelzeile darueber), ist die Sorte
Hilfsbereitschaft, die spaeter niemand nachvollzieht. */
if (ausExcel) {
melde(`Excel gelesen: Blatt „${ausExcel.blatt}", ${ausExcel.zeilen} Zeilen, `
+ `${ausExcel.spalten} Spalten`
+ (ausExcel.uebersprungen ? ` (${ausExcel.uebersprungen} Titelzeile übersprungen)` : ''));
setTimeout(() => melde(''), 7000);
}
await netzVorschau();
} catch {
$('netz-fehler').textContent = 'Die Datei konnte nicht gelesen werden.';
}
});
/* Beim EINFÜGEN sofort nachsehen -- nicht erst auf einen Knopf warten.
Wer eine Tabelle einfügt, will wissen, ob sie erkannt wurde; ein
zweiter Klick dazwischen ist nur eine Gelegenheit, es nicht zu tun. */
$('netz-text').addEventListener('paste', () => setTimeout(netzVorschau, 60));
$('netz-text').addEventListener('change', netzVorschau);
$('netz-tag').addEventListener('change', netzVorschau);
$('netz-form').addEventListener('submit', netzUebernehmen);
(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();
})();
})();