/* =================================================================== 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; } function kachel(zahl, name, { trendEl = null, warn = false } = {}) { const k = el('div', 'kachel'); 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; } 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) })); k1.append(kachel(d.aufgaben.angelegt, 'neu angelegt', { trendEl: trend(d.aufgaben.angelegt, d.aufgaben.angelegt_davor) })); k1.append(kachel(d.termine.gewesen, 'Termine gewesen')); k1.append(kachel(d.dateien.freigegeben, 'Dateien freigegeben')); ziel.append(block('Was wurde erledigt?', null, k1)); /* Was blockiert? */ const k2 = el('div', 'kacheln'); k2.append(kachel(d.aufgaben.offen, 'Aufgaben offen')); k2.append(kachel(d.aufgaben.ueberfaellig, 'überfällig', { warn: d.aufgaben.ueberfaellig > 0 })); const dringend = Object.values(d.bereiche).reduce((s, b) => s + b.dringend, 0); k2.append(kachel(dringend, 'dringende Punkte', { warn: dringend > 0 })); k2.append(kachel(d.dateien.offen_review, 'Dateien im 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)`); 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.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')); for (const [b, w] of Object.entries(d.bereiche)) { if (w.offen) k4.append(kachel(w.offen, BEREICHSNAME[b] + ': offen', { warn: w.dringend > 0 })); } 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; } $('wer').textContent = ich.name + ' · ' + (ich.rolle_name || ich.rolle); 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) { $('unterzeile').textContent = 'Dir ist noch kein Creator zugeteilt. DogFather legt fest, wen du betreust.'; 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(); })(); })();