Filipe hat drei Sachen gemeldet. Zwei davon waren nicht das, wonach sie
aussahen.
1. "BEI DER SPICY ROLLE IST DA EIN PROBLEM" (Creator-Profil laedt nicht)
Nicht diese Seite war kaputt. In NEUN Skripten stand dieselbe Zeile
const LEITUNG = new Set(['admin', 'manager']);
und in keinem davon 'spicy'. profil.js nahm deshalb den Creator-Zweig
und lud das EIGENE Profil -- ein Spicy-Zugang ist kein Creator, also
404. Acht der neun sind nicht kaputtgegangen; sie haben sich still
falsch verhalten.
Die Liste steht jetzt EINMAL in bereiche.js. Beim Aufraeumen kamen mit
derselben Suche sechs weitere Luecken heraus, alle vom selben Typ:
* Aufgaben abbrechen -- Server UND Knopf ohne 'spicy'
* Teilen-Knopf in der Kopfleiste -- ohne 'spicy'
* Chat: die Gespraechsliste laeuft ueber eine feste Rollenfolge.
Wer nicht darin steht, taucht gar nicht auf -- eine Person, die es
fuer die anderen nicht gibt.
* Kalender: dieselbe Rollenfolge, dort landete Spicy Media durch
indexOf() === -1 ganz oben statt an ihrem Platz.
* Dashboard: "Creator anlegen" nur fuer admin/manager -- der Server
haette sie gelassen, den Knopf hat sie nie gesehen.
* Personenliste: ROLLENNAME ohne 'spicy'. Der Auffangwert
`|| p.rolle` schrieb "spicy" statt "Spicy Media" -- plausibel
genug, um jahrelang zu bleiben.
`pruef-css-klassen.mjs` verlangt jetzt, dass kein Workspace-Skript sich
wieder eine eigene Rollenliste baut. Mit Gegenprobe, dass der Sucher so
eine Liste auch wirklich findet.
2. "DIE ZAHLEN DIESE KATEGORIE NICHT SEHEN"
Kachel und Seite waren beim Anlegen der Rolle auf ["spicy","admin"]
gesetzt worden -- "dieselben Rechte ausser Automationen". Beides jetzt
auf DogFather allein.
3. "IN CIGDEMS KALENDER STEHT IMMER NOCH MEIN MANAGER MEETING"
Das war MEIN Denkfehler von heute Frueh, nicht ein vergessener Fall.
Ich hatte "geht alle an" als "kein Teilnehmer eingetragen" definiert --
also entschied der Server, was alle angeht, und nicht der, der den
Termin anlegt. Wer niemanden eintraegt, meint aber meistens nicht
"alle", sondern "mich".
Der Fall ist entfallen. Sichtbar ist ein Termin jetzt nur noch fuer
den, der ihn angelegt hat, dem er zugeordnet ist, der das Gegenueber
ist -- oder der in der Teilnehmerliste steht. Damit ist das Markieren
im Termin die einzige Antwort auf "wen geht das an": eine sichtbare
Entscheidung im Formular statt einer unsichtbaren Regel im Server.
Calls & Protokolle rufen dieselbe Funktion auf und koennen deshalb gar
nicht auseinanderlaufen -- das war Filipes vierter Wunsch, und er ist
mit derselben Zeile erledigt.
Am laufenden System als Cigdem nachgemessen: Profil laedt ("Profil:
Luna"), Zahlen-Kachel weg und Seite gesperrt, im Kalender NUR der
Termin, bei dem sie markiert ist, Calls zeigt genau denselben.
Fuenf Pruefungen auf die endgueltige Regel umgeschrieben statt
geloescht (sicht, spicy, teilnehmer, scout-zuteilung, css-klassen).
Die dritte Drehung bei scout-zuteilung steht mit allen drei Staenden
in der Akte -- eine geloeschte Pruefung hinterlaesst keine Spur davon,
dass hier einmal etwas anderes galt.
Co-Authored-By: Claude Opus 5 <[email protected]>
610 lines
22 KiB
JavaScript
610 lines
22 KiB
JavaScript
/* =====================================================================
|
||
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();
|
||
|
||
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();
|
||
})();
|
||
})();
|