Files
dogfather-universe/workspace/assets/js/report.js
T
DogFatherGitandClaude Opus 5 540b5d8838 Freie Namen, ein Call-Knopf, und drei Seiten, die fuer Scouts kaputt waren
DREI SACHEN AUF EINMAL, alle aus derselben Sitzung.

1. AUSSUCHEN ODER SELBST EINTRAGEN
   Wunsch: "ich will da auch sachen selber noch eintragen koennen, also
   aussuchen und selbst eintragen."

   Jede Personenauswahl (Kalender, Aufgaben, Bereiche, Content, Dateien)
   nimmt jetzt auch einen getippten Namen an -- eine Agentur, eine Marke,
   einen Gast ohne Konto. Dazu bekommt JEDE Auswahl ab acht Eintraegen
   ein Suchfeld: tippen statt scrollen.

   Gebaut IM vorhandenen Auswahl-Bauteil (wahl.js), nicht daneben. Der
   erste Anlauf war ein zweites Bauteil -- es hat sich prompt mit dem
   ersten gebissen, beide haben denselben <select> eingepackt. Ein
   zweites haette ausserdem anders ausgesehen und waere beim naechsten
   Umbau nur an einer von zwei Stellen nachgezogen worden.

   Der freie Name steht in einer EIGENEN Spalte je Feld; die Verknuepfung
   bleibt leer. Entweder eine Person ODER ein Name, nie beides.

   Filipe hat ausdruecklich auch bei den Creator-Feldern freie Namen
   gewollt, nachdem der Nachteil benannt war: Der Eintrag gehoert dann zu
   keinem Konto. Damit daraus kein STILLER Ausfall wird, faellt jede
   Abfrage, die bisher den Namen der verknuepften Person las, jetzt auf
   den freien Text zurueck (externSql) -- gekennzeichnet als "(extern)".
   Der Eintrag verschwindet dadurch aus keiner Liste, keiner Suche und
   keiner Uebersicht.

2. WO FUEHRE ICH DEN CALL?
   Den Knopf gab es, aber nur wenn jemand von Hand einen Link ins
   Ortsfeld getippt hatte UND das Gespraech noch bevorstand. Stand dort
   "Hier", war nichts zum Anklicken da.

   Jetzt hat das Team einen festen Call-Raum (Discord-Sprachkanal), den
   das Management einmal hinterlegt. Danach hat JEDER Call den Knopf --
   und er bleibt, solange das Gespraech laufen kann, nicht nur bis zur
   Startzeit. Ein eigener Link am Termin schlaegt den festen Raum.

3. WAS EINE ROLLE SIEHT, MUSS AUCH FUNKTIONIEREN
   Gemeldet: "cigdem kriegt als manager gewisse sachen nicht auf die sie
   sieht, check jede rolle ab."

   Neue Pruefung server/pruef-rollen.mjs schickt SECHS Rollen-Zustaende
   ueber alle 16 Seiten und misst Konsolenfehler, fehlgeschlagene
   Serveraufrufe, tote Verweise, haengende Ladeanzeigen und wortlos leere
   Seiten. 96 Durchgaenge.

   Der sechste Zustand ist der wichtige: eine Rolle OHNE zugeteilte
   Creator -- der Normalfall am ersten Tag und Cigdems echte Lage.
   Genau dort fielen die Seiten durch, waehrend dieselben Seiten MIT
   Zuteilung tadellos waren.

   VIER ECHTE FEHLER GEFUNDEN UND BEHOBEN:

   a) Eine Aufgabe, die eine Managerin ohne Creator anlegte, war fuer sie
      im selben Moment unsichtbar -- creator_id und verantwortlich_id
      leer, und "von mir selbst angelegt" stand in keiner
      Sichtbarkeitsregel. Nur DogFather sah sie noch. Kein Fehler, keine
      Meldung, die Aufgabe war einfach weg. Dasselbe bei den
      Bereichseintraegen. Beide Regeln kennen jetzt erstellt_von.
      Niemand sieht dadurch etwas Fremdes -- nur das Eigene.

   b) Ein Scout ohne zugeteilten Creator bekam auf die GESAMTE
      Report-Seite 404, obwohl sie fuer ihn verlinkt ist. Die Seite
      antwortet jetzt sauber und leer, statt sich zu verweigern.

   c) Start-Check und Report blieben fuer immer auf "wird geladen"
      stehen, wenn es nichts zu laden gab.

   d) Das Creator-Profil war fuer Scouts ohne Zuteilung wortlos leer --
      der erklaerende Satz stand nur in der grauen Unterzeile.

   Ausserdem meldete die bestehende Lesbarkeitspruefung zwei neue
   Beschriftungen von mir als zu klein fuers Handy (10,88 statt 11,5 px).
   Behoben, und dieselbe Groesse an der Serien-Karte gleich mit -- dort
   waere es erst aufgefallen, sobald jemand eine Wiederholung anlegt.

GEPRUEFT: pruef-rollen 97, pruef-freie-namen 32 (mit Gegenproben:
Ben sieht Cigdems Aufgabe NICHT; die Saeulen-Zuordnung nimmt
ausdruecklich KEINEN freien Namen). Alle bestehenden Laeufe gruen:
Startansicht 133, Kalender 84, Serien 67, Protokoll 52, Handy 50, Sicht
48, Ampel 47, Content 45, Aufgabenbrett 44, Sprung 43,
Personen-Loeschen 40, Bereiche 37, Uebersicht 33, Workspace-Seiten 32,
Formulare 19, Betreuung 18, Grosscheck 15, Lesbarkeit 14.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-02 20:10:09 +02:00

338 lines
14 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.
/* ===================================================================
Reports & Review.
Die Fragen der Abschnitte stammen woertlich aus dem Konzept, Seite 16:
"Was wurde erledigt? Was blockiert? Was hat funktioniert? Was kommt
naechste Woche?"
=================================================================== */
(() => {
'use strict';
/* DogFather und Manager haben dieselben Rechte -- die Unterscheidung
macht nur die Personenverwaltung, und die prueft der Server. */
const LEITUNG = new Set(['admin', 'manager']);
const $ = (id) => document.getElementById(id);
const BEREICHSNAME = {
live: 'LIVE', content: 'Content', technik: 'Technik',
community: 'Community', schutz: 'Schutz',
};
let ich = null;
let kiDa = false;
const el = (tag, klasse, text) => {
const k = document.createElement(tag);
if (klasse) k.className = klasse;
if (text !== undefined) k.textContent = text;
return k;
};
const melde = (t) => { $('fehler').textContent = t || ''; };
const datum = (iso) => (iso ? iso.slice(0, 10).split('-').reverse().join('.') : '—');
async function hole(pfad, optionen = {}) {
const a = await fetch(pfad, { credentials: 'same-origin', ...optionen });
if (a.status === 401) { location.assign('/workspace/'); throw new Error('abgemeldet'); }
if (a.status === 404) { location.assign('/workspace/start.html'); throw new Error('kein Zugriff'); }
return a;
}
/* Vorher/Nachher. `gut` sagt, ob ein Anstieg erfreulich ist -- bei
offenen Problemen ist er das nicht. */
function trend(jetzt, davor, gut = true) {
const t = el('span', 'trend');
t.dataset.gut = gut ? 'ja' : 'nein';
const diff = jetzt - davor;
if (!davor && !jetzt) { t.dataset.richtung = 'gleich'; t.textContent = '–'; return t; }
if (diff === 0) { t.dataset.richtung = 'gleich'; t.textContent = 'unverändert'; return t; }
t.dataset.richtung = diff > 0 ? 'hoch' : 'runter';
t.textContent = (diff > 0 ? '▲ +' : '▼ ') + diff + ' ggü. davor';
return t;
}
/* BEWUSST OHNE die Creator-Auswahl im Verweis. Der erste Entwurf hängte
?creator=<nr> an -- das sah hilfreich aus und wäre eine Lüge gewesen:
Keine der Zielseiten liest diesen Wert. Aufgabenbrett, Dateien und
Bereiche zeigen immer alles, was die angemeldete Person sehen darf.
Ein Parameter, der nichts bewirkt, wird beim nächsten Mal für
lebenden Code gehalten und mitgepflegt.
Wenn diese Seiten einmal nach Creator filtern können, gehört er
wieder dazu -- dann aber zusammen mit der Stelle, die ihn ausliest. */
/* Eine Kennzahl. `ziel` macht daraus einen Weg zu genau der Stelle,
an der die gezaehlten Dinge stehen (Wunsch 01.09.2026: "wenn man
auf die Kisten drueckt, sofort zu diesem Punkt weitergeschickt
werden").
Eine Zahl ohne Weg dahin ist eine Behauptung: Man kann sie nur
glauben oder von Hand nachsuchen. */
function kachel(zahl, name, { trendEl = null, warn = false, ziel = null } = {}) {
const k = el(ziel ? 'a' : 'div', 'kachel' + (ziel ? ' kachel--weg' : ''));
if (ziel) {
k.href = ziel;
/* Der ganze Satz fuer Vorleseprogramme -- "3" allein sagt nichts,
und ein Verweis ohne erkennbares Ziel ist fuer sie wertlos. */
k.setAttribute('aria-label', `${name}: ${zahl} – ansehen`);
}
if (warn) k.dataset.warn = 'ja';
const wert = el('div', 'kachel__wert');
wert.append(el('span', 'kachel__zahl', String(zahl)));
if (trendEl) wert.append(trendEl);
k.append(wert, el('span', 'kachel__name', name));
return k;
}
/* Wohin fuehrt ein Betreuungsbereich? Drei liegen auf bereich.html,
Content hat eine eigene Seite. Die Zuordnung steht EINMAL hier --
zweimal abgeschrieben waere sie beim naechsten neuen Bereich an
einer der beiden Stellen vergessen. */
const BEREICH_ZIEL = {
live: 'bereich.html?b=live',
community: 'bereich.html?b=community',
technik: 'bereich.html?b=technik',
schutz: 'bereich.html?b=schutz',
content: 'content.html',
};
const bereichZiel = (b, zeigen) => {
const z = BEREICH_ZIEL[b];
if (!z) return null;
return zeigen ? z + (z.includes('?') ? '&' : '?') + 'zeigen=' + zeigen : z;
};
function block(titel, frage, inhalt) {
const b = el('section', 'block');
b.append(el('h2', 'block__titel', titel));
if (frage) b.append(el('p', 'block__frage', frage));
b.append(inhalt);
return b;
}
/* ---------- Bericht zeichnen ------------------------------------------- */
function zeichne(d) {
const ziel = $('bericht');
ziel.textContent = '';
ziel.setAttribute('aria-busy', 'false');
$('unterzeile').textContent =
`${d.creator_name ? d.creator_name + ' · ' : 'Alle Creator · '}`
+ `${datum(d.zeitraum.ab)} bis ${datum(d.zeitraum.bis)}`
+ ` · Vergleich mit den ${d.zeitraum.tage} Tagen davor`;
/* Was wurde erledigt? */
const k1 = el('div', 'kacheln');
k1.append(kachel(d.aufgaben.erledigt, 'Aufgaben erledigt',
{ trendEl: trend(d.aufgaben.erledigt, d.aufgaben.erledigt_davor),
ziel: 'aufgaben.html?zeigen=erledigt' }));
/* "neu angelegt" fuehrt auf das Brett OHNE Ausschnitt: Der Bericht
zaehlt hier einen Zeitraum, das Brett kennt keinen. Ein Sprung auf
"zeigen=angelegt" wuerde etwas hervorheben, das nicht dieselbe
Menge ist -- und eine Hervorhebung, die nicht stimmt, ist
schlimmer als keine. */
k1.append(kachel(d.aufgaben.angelegt, 'neu angelegt',
{ trendEl: trend(d.aufgaben.angelegt, d.aufgaben.angelegt_davor),
ziel: 'aufgaben.html' }));
k1.append(kachel(d.termine.gewesen, 'Termine gewesen',
{ ziel: 'kalender.html?zeigen=vergangen' }));
k1.append(kachel(d.dateien.freigegeben, 'Dateien freigegeben',
{ ziel: 'dateien.html?zeigen=frei' }));
ziel.append(block('Was wurde erledigt?', null, k1));
/* Was blockiert? */
const k2 = el('div', 'kacheln');
k2.append(kachel(d.aufgaben.offen, 'Aufgaben offen',
{ ziel: 'aufgaben.html?zeigen=offen' }));
k2.append(kachel(d.aufgaben.ueberfaellig, 'überfällig',
{ warn: d.aufgaben.ueberfaellig > 0, ziel: 'aufgaben.html?zeigen=ueberfaellig' }));
const dringend = Object.values(d.bereiche).reduce((s, b) => s + b.dringend, 0);
/* Diese Zahl ist eine SUMME ueber alle Bereiche -- es gibt keine
einzelne Seite, auf der genau sie steht. Sie fuehrt deshalb in den
Bereich, in dem die meisten davon liegen, statt blind in die
LIVE-Analyse. Liegt nichts an, fuehrt sie nirgendwohin: ein Weg zu
null Dingen ist eine Enttaeuschung, kein Angebot. */
const meiste = Object.entries(d.bereiche)
.filter(([, w]) => w.dringend > 0)
.sort((a, b) => b[1].dringend - a[1].dringend)[0];
k2.append(kachel(dringend, 'dringende Punkte',
{ warn: dringend > 0, ziel: meiste ? bereichZiel(meiste[0], 'dringend') : null }));
k2.append(kachel(d.dateien.offen_review, 'Dateien im Review',
{ ziel: 'dateien.html?zeigen=review' }));
const b2 = block('Was blockiert?', 'Die drei Aufgaben, die am längsten offen liegen:', k2);
if (d.aufgaben.aeltester.length) {
const liste = el('ul', 'liste-klein');
for (const a of d.aufgaben.aeltester) {
const li = el('li');
li.append(document.createTextNode(a.titel));
li.append(el('span', 'klein',
`offen seit ${datum(a.erstellt)}${a.frist ? ' · Frist ' + datum(a.frist) : ''}`));
liste.append(li);
}
b2.append(liste);
} else {
b2.append(el('p', 'leer-hinweis', 'Nichts offen – sauber.'));
}
ziel.append(b2);
/* Was hat funktioniert? */
const k3 = el('div', 'kacheln');
const jetztS = d.live.jetzt.schnitt;
const davorS = d.live.davor.schnitt;
const kLive = kachel(jetztS === null ? '–' : String(jetztS).replace('.', ','),
`LIVE-Schnitt aus ${d.live.jetzt.anzahl} Bewertung(en)`,
{ ziel: d.live.jetzt.anzahl > 0 ? bereichZiel('live') : null });
if (jetztS !== null && davorS !== null) {
kLive.querySelector('.kachel__wert').append(
trend(Math.round(jetztS * 10), Math.round(davorS * 10)));
}
k3.append(kLive);
for (const [b, w] of Object.entries(d.bereiche)) {
k3.append(kachel(w.neu, BEREICHSNAME[b] + ': neue Einträge',
{ trendEl: trend(w.neu, w.neu_davor), ziel: bereichZiel(b) }));
}
ziel.append(block('Was hat funktioniert?',
'Bewegung in den Betreuungsbereichen im Vergleich zum Zeitraum davor:', k3));
/* Was kommt als Nächstes? */
const k4 = el('div', 'kacheln');
k4.append(kachel(d.termine.kommend, 'Termine stehen an',
{ ziel: 'kalender.html?zeigen=kommend' }));
for (const [b, w] of Object.entries(d.bereiche)) {
if (w.offen) k4.append(kachel(w.offen, BEREICHSNAME[b] + ': offen',
{ warn: w.dringend > 0, ziel: bereichZiel(b, 'offen') }));
}
const b4 = block('Was kommt als Nächstes?', null, k4);
if (d.profil && (d.profil.plan_prio1 || d.profil.plan_prio2 || d.profil.plan_prio3)) {
const liste = el('ul', 'liste-klein');
for (const [i, t] of [d.profil.plan_prio1, d.profil.plan_prio2, d.profil.plan_prio3].entries()) {
if (!t) continue;
const li = el('li');
li.append(document.createTextNode(t));
li.append(el('span', 'klein', `Priorität ${i + 1} aus dem 90-Tage-Plan`));
liste.append(li);
}
b4.append(liste);
}
if (d.profil?.naechster_review) {
b4.append(el('p', 'block__frage', 'Nächster Review-Termin: ' + datum(d.profil.naechster_review)));
}
ziel.append(b4);
$('entscheidung-block').hidden = false;
/* Zahlen in Worte -- erst auf Knopfdruck, nie von selbst. Der
Report ist ohne KI vollstaendig; das hier ist eine Zugabe. */
if (kiDa) {
const reihe = el('div', 'ki-reihe');
reihe.append(window.KI.knopf('In Worte fassen', async () => {
const zahlen = `Zeitraum ${d.zeitraum.tage} Tage`
+ `, ${d.aufgaben.erledigt} Aufgaben erledigt (davor ${d.aufgaben.erledigt_davor})`
+ `, ${d.aufgaben.angelegt} neu angelegt (davor ${d.aufgaben.angelegt_davor})`
+ `, ${d.aufgaben.offen} offen, ${d.aufgaben.ueberfaellig} überfällig`
+ `, ${Object.values(d.bereiche).reduce((s, b) => s + b.dringend, 0)} dringende Punkte`
+ `, LIVE-Schnitt ${d.live.jetzt.schnitt ?? '-'} aus ${d.live.jetzt.anzahl} Bewertungen`
+ `, ${d.termine.gewesen} Termine gewesen, ${d.termine.kommend} stehen an.`;
const r = await window.KI.frage('reporttext', { zahlen });
const alt = reihe.querySelector('.ki-text');
if (alt) alt.remove();
const text = el('p', 'ki-text', r.text);
reihe.append(text);
}));
ziel.append(reihe);
}
}
async function laden() {
melde('');
const creator = $('f-creator').value || 'alle';
const tage = $('f-tage').value;
try {
const a = await hole(`/workspace/api/report?creator=${encodeURIComponent(creator)}&tage=${tage}`);
if (!a.ok) { melde('Report konnte nicht erstellt werden.'); return; }
zeichne(await a.json());
} catch { /* umgeleitet */ }
}
/* ---------- Entscheidung ------------------------------------------------ */
$('entscheidung').addEventListener('submit', async (e) => {
e.preventDefault();
$('e-fehler').textContent = '';
const knopf = $('e-speichern');
knopf.disabled = true;
try {
const a = await hole('/workspace/api/report/entscheidung', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
titel: $('e-titel').value,
frist: $('e-frist').value,
creator_id: $('f-creator').value || null,
}),
});
if (!a.ok) {
$('e-fehler').textContent = (await a.json().catch(() => ({}))).fehler || 'Ging nicht.';
return;
}
$('entscheidung').reset();
$('e-stand').textContent = 'Als Aufgabe angelegt ✓';
setTimeout(() => { $('e-stand').textContent = ''; }, 3200);
await laden();
} catch { /* umgeleitet */ } finally { knopf.disabled = false; }
});
/* ---------- Start -------------------------------------------------------- */
(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);
kiDa = await window.KI.verfuegbar();
try {
const { creator } = await (await hole('/workspace/api/report/creator')).json();
if (LEITUNG.has(ich.rolle)) {
$('creator-block').hidden = false;
const alle = document.createElement('option');
alle.value = ''; alle.textContent = 'Alle Creator';
$('f-creator').append(alle);
} else if (ich.rolle === 'scout') {
/* Ein Scout waehlt zwischen SEINEN Creator -- "alle" gibt es
fuer ihn nicht, das waere die Gesamtuebersicht. */
if (!creator.length) {
/* Nicht nur die Unterzeile beschriften: Der Berichtsbereich
stand danach für immer auf "Report wird erstellt …", weil
ihn nichts mehr anfasste. Für ein Vorleseprogramm ist eine
dauerhaft beschäftigte Fläche das Zeichen, hier nicht
weiterzulesen. Gefunden beim Rollen-Rundgang 02.09.2026. */
const satz = 'Dir ist noch kein Creator zugeteilt. '
+ 'DogFather legt fest, wen du betreust.';
$('unterzeile').textContent = satz;
const ziel = $('bericht');
ziel.setAttribute('aria-busy', 'false');
ziel.textContent = '';
const hinweis = document.createElement('p');
hinweis.className = 'leer-hinweis';
hinweis.textContent = satz;
ziel.append(hinweis);
return;
}
$('creator-block').hidden = false;
}
for (const c of creator) {
const o = document.createElement('option');
o.value = String(c.id); o.textContent = c.name;
$('f-creator').append(o);
}
if (ich.rolle === 'creator') $('f-creator').value = String(ich.id);
} catch { return; }
$('f-creator').addEventListener('change', laden);
$('f-tage').addEventListener('change', laden);
await laden();
})();
})();