/* =================================================================== Aufgabenbrett. Der Server entscheidet, wer was sehen und ändern darf -- diese Datei zeigt nur an. Was hier ausgeblendet wird, ist Bequemlichkeit, kein Schutz. =================================================================== */ (() => { 'use strict'; /* DogFather und Manager haben dieselben Rechte -- die Unterscheidung macht nur die Personenverwaltung, und die prueft der Server. */ /* 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; /* `leer` ist der Satz, der in einer leeren Spalte steht. Vorher stand dort in allen vier Spalten ein Gedankenstrich -- vier graue Striche nebeneinander sehen aus, als sei die Seite kaputt, obwohl gar nichts kaputt ist. Jeder Satz sagt jetzt, was das Leersein BEDEUTET: eine leere Freigabe-Spalte ist etwas anderes als eine leere Erledigt-Spalte. `unter` erklärt die Spalte auch dann, wenn sie voll ist. */ const SPALTEN = [ { status: 'offen', name: 'Offen', unter: 'Angenommen, noch nicht angefangen', leer: 'Nichts liegt herum.' }, { status: 'arbeit', name: 'In Arbeit', unter: 'Läuft gerade', leer: 'Gerade läuft nichts.' }, /* AUS 'REVIEW' WURDE 'ZUR FREIGABE' (20.09.2026). Weiter unten stand die Begruendung schon selbst da: 'Review' allein sagt einem Neuen nichts. Es ist englisch, es ist ein Hauptwort ohne Handlung, und es verraet nicht, WER jetzt dran ist. 'Zur Freigabe' sagt beides: fertig von mir, wartet auf jemanden. Der Schluessel bleibt `review` -- der gehoert der Datenbank, nicht dem Menschen. */ { status: 'review', name: 'Zur Freigabe', unter: 'Wartet darauf, dass jemand draufschaut', leer: 'Nichts wartet auf Freigabe.' }, { status: 'erledigt', name: 'Erledigt', unter: 'Fertig und abgehakt', leer: 'Hier landet, was fertig ist.' }, ]; const WEITER = { offen: 'arbeit', arbeit: 'review', review: 'erledigt' }; const ZURUECK = { arbeit: 'offen', review: 'arbeit', erledigt: 'review' }; /* Wer darf abbrechen (05.09.2026): *"nur ich die manager und scouts sollen auch die aufgaben abbrechen können."* Ein Creator nicht. Dieselbe Menge steht im Server (DARF_ABBRECHEN in workspace-aufgaben.js) und DORT sitzt die Sperre. Hier geht es nur darum, niemandem einen Knopf hinzustellen, der dann 403 sagt. */ /* Leitung plus Scouts. Aus der einen Rollenquelle abgeleitet statt abgeschrieben -- die abgeschriebene Fassung hatte 'spicy' vergessen, und ein fehlender Knopf faellt niemandem auf. */ const DARF_ABBRECHEN = new Set([...window.Bereiche.LEITUNG, 'scout']); const STATUS_NAME = { offen: 'Offen', arbeit: 'In Arbeit', review: 'Zur Freigabe', erledigt: 'Erledigt', }; const $ = (id) => document.getElementById(id); /* ORTSZEIT, nicht UTC -- sonst gilt nachts eine heute faellige Aufgabe noch nicht als faellig, und "Heute faellig" zeigt den Vortag. */ const heute = window.heuteLokal(); let ich = null; /* Nur beim ERSTEN Laden springen. Sonst reisst es die Seite nach jeder Statusaenderung erneut an dieselbe Stelle. */ let gesprungen = false; /* Texte werden ausschließlich über textContent gesetzt, nie über innerHTML -- ein Aufgabentitel darf keine Auszeichnung einschleusen. */ 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 = (text) => { $('fehler').textContent = text || ''; }; async function hole(pfad, optionen = {}) { const antwort = await fetch(pfad, { credentials: 'same-origin', ...optionen }); if (antwort.status === 401) { location.assign('/workspace/'); throw new Error('abgemeldet'); } return antwort; } /* ---------- Brett zeichnen ------------------------------------------ */ function datumHuebsch(iso) { const [j, m, t] = iso.split('-'); return `${t}.${m}.${j.slice(2)}`; } /* ---------- Rückmeldungen ------------------------------------------------ Direkt an der Karte, nicht in einem Fenster: Man liest sie im Zusammenhang mit der Aufgabe, sonst fehlt beim Antworten die Hälfte. */ const zeitpunkt = (iso) => { if (!iso) return ''; const d = new Date(iso); const p = (n) => String(n).padStart(2, '0'); return `${p(d.getDate())}.${p(d.getMonth() + 1)}. · ${p(d.getHours())}:${p(d.getMinutes())}`; }; async function rueckmeldungen(karteEl, a, knopfEl) { const vorhanden = karteEl.querySelector('.rueck'); if (vorhanden) { vorhanden.remove(); delete knopfEl.dataset.offen; return; } knopfEl.dataset.offen = 'ja'; const box = el('div', 'rueck'); box.append(el('p', 'leise', 'Wird geladen …')); karteEl.append(box); let daten; try { const antwort = await hole(`/workspace/api/aufgaben/${a.id}/notizen`); if (!antwort.ok) { box.textContent = ''; box.append(el('p', 'fehler', 'Konnte nicht geladen werden.')); return; } daten = await antwort.json(); } catch { return; } box.textContent = ''; const liste = el('div', 'rueck__liste'); if (!daten.notizen.length) liste.append(el('p', 'leise', 'Noch nichts besprochen.')); for (const n of daten.notizen) { const z = el('div', 'rueck__eintrag'); const kopf = el('p', 'rueck__kopf'); kopf.append(el('span', 'rueck__wer', n.von || 'Unbekannt')); kopf.append(el('span', 'rueck__wann', zeitpunkt(n.erstellt))); /* Die eigene Rückmeldung darf jeder zurücknehmen, fremde nur die Leitung -- wer sich vertippt hat, soll das nicht beantragen müssen. */ if (n.person_id === daten.ich || daten.darf_alles_loeschen) { const weg = el('button', 'rueck__weg', '×'); weg.type = 'button'; weg.title = 'Rückmeldung entfernen'; weg.setAttribute('aria-label', 'Rückmeldung entfernen'); weg.addEventListener('click', async () => { if (!await frageNach({ titel: 'Rückmeldung entfernen?', was: 'Sie verschwindet aus der Aufgabe.', bleibt: 'Die Aufgabe selbst bleibt unverändert stehen.', endgueltig: true, ja: 'Entfernen', })) return; const r = await hole('/workspace/api/notizen/' + n.id, { method: 'DELETE' }); if (!r.ok) { melde('Entfernen hat nicht geklappt.'); return; } await laden(); }); kopf.append(weg); } z.append(kopf, el('p', 'rueck__text', n.text)); liste.append(z); } box.append(liste); const form = el('form', 'rueck__form'); const feld = el('textarea', 'rueck__feld'); feld.rows = 2; feld.maxLength = 2000; feld.placeholder = 'Rückmeldung schreiben …'; const senden = el('button', 'knopf knopf--klein', 'Abschicken'); senden.type = 'submit'; form.append(feld, senden); form.addEventListener('submit', async (e) => { e.preventDefault(); const text = feld.value.trim(); if (text.length < 2) return; senden.disabled = true; try { const r = await hole(`/workspace/api/aufgaben/${a.id}/notizen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text }), }); if (!r.ok) { melde(sagWas((await r.json().catch(() => ({}))).fehler, 'Ging nicht.')); return; } await laden(); } catch { /* umgeleitet */ } finally { senden.disabled = false; } }); box.append(form); feld.focus(); } /* ===================================================================== EINE KARTE IST EIN STUECK ARBEIT -- NICHT EINE TABELLENZEILE (23.09.2026) Filipe: "so wie alles gerade ist ist es zu wenig und zu viel zu gleich." Das ist genau richtig beobachtet, und die Zahlen dahinter sind diese (am 23.09.2026 in der echten Datenbank nachgemessen): 115 offene Aufgaben -- aber nur 17 VERSCHIEDENE vier Modis, jede Aufgabe an jede verteilt -> x4 und die Verteilung lief ZWEIMAL -> x8 Das Brett zeigte daraufhin 136 Karten in EINER Spalte, waehrend drei Spalten leer danebenstanden. Gemessen war die Seite 46 077 px hoch bei 950 px Bildschirm -- 48 Bildschirme Scrollen. Am Handy 53. Das ist das "zu viel". Das "zu wenig" ist dasselbe von der anderen Seite: Nirgends stand, WAS eigentlich zu tun ist. Man musste 136 Karten lesen, um 17 Dinge zu erfahren. WAS SICH AENDERT: Aufgaben mit demselben Titel, derselben Frist und derselben Kategorie sind EIN Stueck Arbeit, an mehrere verteilt. Sie bekommen EINE Karte mit den Leuten darauf. Aus 136 Karten werden 17. WARUM DAS NICHT NUR ZUSAMMENFASSEN IST: Auf der Sammelkarte sieht man etwas, das vorher NICHT sichtbar war -- naemlich wie weit die Gruppe ist ("0 von 4") und wer noch nicht angefangen hat. Vorher musste man vier Karten suchen, die 30 Karten auseinanderlagen. UND SIE ZEIGT DIE DOPPELTEN. Steht jemand zweimal in derselben Gruppe, ist die Aufgabe doppelt verteilt worden. Das sieht man jetzt auf einen Blick statt gar nicht. WER ALLEIN AN ETWAS SITZT, bekommt unveraendert seine Karte -- eine Gruppe aus einem Menschen ist einfach eine Karte. Der Sonderfall ist damit kein Sonderfall, sondern der Normalfall mit der Zahl 1. ===================================================================== */ /* Wie weit ein Status ist. Die Gruppe steht dort, wo ihr am WENIGSTEN fortgeschrittenes Mitglied steht -- denn das ist die Arbeit, die noch zu tun ist. Wuerde die Gruppe bei "erledigt" stehen, sobald EINE Person fertig ist, verschwaende sie aus dem Blick, obwohl drei noch nicht angefangen haben. */ const RANG = { offen: 0, arbeit: 1, review: 2, erledigt: 3 }; /** Aus einer Liste Aufgaben werden Stuecke Arbeit. */ function gruppieren(liste) { const nach = new Map(); for (const a of liste) { /* DER SCHLUESSEL IST DAS, WAS EIN MENSCH SIEHT: derselbe Titel, dieselbe Frist, dieselbe Kategorie. Nicht das Vorlagenfeld -- das ist bei beiden Verteilrunden gleich und wuerde die Doppelten unsichtbar machen, statt sie zu zeigen. \u0000 als Trenner, weil es in keinem Titel vorkommt; ein Bindestrich koennte selbst im Titel stehen. */ const schluessel = [a.titel, a.frist || '', a.kategorie || ''].join('\u0000'); let g = nach.get(schluessel); if (!g) { g = { schluessel, alle: [] }; nach.set(schluessel, g); } g.alle.push(a); } for (const g of nach.values()) { /* Die Karte zeigt die Angaben der am wenigsten fortgeschrittenen Aufgabe -- dieselbe, die auch die Spalte bestimmt. */ g.alle.sort((x, y) => (RANG[x.status] ?? 0) - (RANG[y.status] ?? 0) || x.id - y.id); g.erste = g.alle[0]; g.status = g.erste.status; g.fertig = g.alle.filter((a) => a.status === 'erledigt').length; /* Wer ist beteiligt -- und wer steht doppelt drin? */ const leute = new Map(); for (const a of g.alle) { const kennung = a.verantwortlich_id != null ? 'p' + a.verantwortlich_id : 'x' + (a.verantwortlich_name || '?'); if (!leute.has(kennung)) { leute.set(kennung, { name: a.verantwortlich_name || 'Niemand', id: a.verantwortlich_id ?? null, aufgaben: [] }); } leute.get(kennung).aufgaben.push(a); } g.leute = [...leute.values()].sort((x, y) => x.name.localeCompare(y.name, 'de')); g.doppelt = g.leute.filter((p) => p.aufgaben.length > 1); } return [...nach.values()]; } function karte(a) { const k = el('article', 'karte'); k.dataset.prio = a.prioritaet; /* DIE NUMMER AN DER KARTE. Ohne sie kann kein Verweis auf eine EINZELNE Aufgabe zeigen -- und drei davon gibt es im Haus. */ k.dataset.id = String(a.id); const kopf = el('div', 'karte__kopf'); kopf.append(el('h3', 'karte__titel', a.titel)); const stift = el('button', 'karte__stift', 'bearbeiten'); stift.type = 'button'; stift.setAttribute('aria-label', `${a.titel} bearbeiten`); stift.addEventListener('click', () => dialogOeffnen(a)); kopf.append(stift); k.append(kopf); /* DIE BESCHREIBUNG DARF GESTALTET SEIN (21.09.2026). Filipe: "... sei es wenn die modis oder rechte hand oder dogfather eine aufgabe oder sachen posten." Dieselbe Quelle wie im Chat und auf den Brettern. `textContent` bleibt der Weg -- `Textform.zeichne` baut Elemente, kein HTML. */ if (a.beschreibung) { const bt = el('p', 'karte__text'); if (window.Textform) bt.append(window.Textform.zeichne(a.beschreibung)); else bt.textContent = a.beschreibung; k.append(bt); } const zeile = el('div', 'karte__zeile'); /* WICHTIG STEHT DA, statt nur eine Kante einzufaerben. Die Prioritaet gab es als 3 px breiten Rand links -- direkt neben der farbigen Kante der Spalte, und damit praktisch unsichtbar. Als Wort steht sie da, wo man sie liest. NUR bei "hoch". Eine Marke an jeder Karte waere keine Marke mehr: Was immer da steht, sieht man nach zwei Tagen nicht mehr. */ if (a.prioritaet === 'hoch' && a.status !== 'erledigt') { zeile.append(el('span', 'karte__wichtig', 'wichtig')); } /* ---- WIE LANGE GEHT DAS SCHON SO? (15.09.2026, Stufe 9) -------- "Ueberfaellig" war ein Schalter: an oder aus. Eine Karte, deren Frist gestern lief, sah aus wie eine, die seit drei Wochen liegt. Stehen auf einem Brett zwanzig Karten "ueberfaellig", sagt keine davon mehr etwas -- man ueberliest sie alle, auch die, die seit einem Monat blockiert. DAS WORT KOMMT VOM SERVER. Eine zweite Rechnung hier waere die, die irgendwann anders antwortet als die Startseite. */ if (a.eskalation) { const e = el('span', 'karte__eskal', a.eskalation.wort); e.dataset.stufe = a.eskalation.stufe; e.title = a.eskalation.grund === 'frist' ? 'Die Frist ist vorbei. Die Zahl sagt, seit wann.' : 'Ohne Frist – seit so vielen Tagen hat niemand die Karte angefasst.'; zeile.append(e); } if (a.frist) { /* Das Wort "ueberfaellig" steht jetzt oben in der Stufe. Hier bleibt nur noch das Datum -- zweimal dasselbe nebeneinander waere Laerm. */ const faellig = !!a.eskalation && a.eskalation.grund === 'frist'; const f = el('span', 'karte__frist' + (faellig ? ' karte__frist--faellig' : ''), 'bis ' + datumHuebsch(a.frist)); zeile.append(f); } /* Die Kategorie als Wort in derselben Zeile wie Frist und Person. Kein eigener Streifen und keine eigene Farbe: Sie ist eine Einordnung, keine Warnung -- und eine Karte, auf der alles hervorsticht, hebt nichts hervor. Der Name kommt aus der Liste des Servers; steht die Aufgabe in einer Kategorie, die es nicht mehr gibt, wird nichts angezeigt statt eines Schluessels. KEINE EIGENE CSS-KLASSE. Sie muesste in sieben gleichlautenden Kopien der Modulliste gepflegt werden (pruef-css-klassen wacht darueber, dass die sieben Zeichen fuer Zeichen gleich sind) -- viel Aufwand und sieben Gelegenheiten fuer einen Unterschied, fuer eine Zeile Text. Dieselbe schlichte Form wie "· Bereich X" daneben tut es. */ /* DER KANAL STEHT VOR DER KATEGORIE. Er beantwortet die groebere Frage -- WO ueberhaupt -- und die muss man zuerst wissen, um die feinere zu verstehen. */ if (a.kanal) { const kName = kanaele.find((k) => k.wert === a.kanal)?.name; if (kName) { const z = el('span', 'aufgabe__kanal', kName); z.dataset.kanal = a.kanal; zeile.append(z); } } if (a.kategorie) { const name = kategorien.find((k) => k.wert === a.kategorie)?.name; if (name) zeile.append(el('span', null, '· ' + name)); } if (a.verantwortlich_name) zeile.append(el('span', null, '· ' + a.verantwortlich_name)); /* WER VERTEILT, DARF AUCH SEHEN, WOHIN (21.09.2026). Dieselbe Frage wie bei den Feldern, also dieselbe Antwort -- und nicht eine Rollenmenge, die daneben altert. */ /* DER SCHALTER „DAUERHAFT" NUR FUER DIE, DIE VERTEILEN (30.09.2026). Ein Modi legt sich Aufgaben selbst an -- eine, die er danach nicht mehr abhaken kann, waere eine Falle, die er sich selbst stellt. Die Schranke sitzt im Server; das hier ist die Hoeflichkeit davor. */ var dfeld = document.getElementById('feld-dauerhaft'); if (dfeld) dfeld.hidden = !(ich.darf_verteilen ?? LEITUNG.has(ich.rolle)); if ((ich.darf_verteilen ?? LEITUNG.has(ich.rolle)) && a.creator_name && a.creator_name !== a.verantwortlich_name) { zeile.append(el('span', null, '· Bereich ' + a.creator_name)); } if (zeile.childElementCount) k.append(zeile); const schritte = el('div', 'karte__schritte'); /* NUR AN KARTEN, DIE MAN AUCH BEWEGEN DARF (20.09.2026). Vorher standen diese zwei Knoepfe an JEDER Karte -- auch an fremden. Ein Modi sieht das Brett des ganzen Teams; er tippte an einer fremden Karte auf „In Arbeit", der Server lehnte mit 403 ab, und die Meldung erschien ganz oben auf der Seite. Bei einer Karte weiter unten sah er nichts: ein Knopf, der einfach nichts tut. `darf_aendern` kommt vom Server -- aus derselben Regel, die auch das Schreiben entscheidet. */ if (a.darf_aendern !== false) { if (ZURUECK[a.status]) schritte.append(knopf('◀ zurück', a, ZURUECK[a.status])); if (WEITER[a.status]) schritte.append(knopf(beschriftung(a.status), a, WEITER[a.status])); } /* ABBRECHEN — in JEDEM Status (05.09.2026), auch im erledigten: Auch etwas Abgehaktes kann sich als hinfällig herausstellen. Nur DogFather, Manager und Scouts. Ein Creator sieht den Knopf gar nicht erst -- die eigentliche Sperre sitzt aber im Server, hier geht es nur darum, niemandem etwas anzubieten, das dann abgelehnt wird. */ if (DARF_ABBRECHEN.has(ich.rolle)) { const ab = el('button', 'schritt schritt--ab', 'abbrechen'); ab.type = 'button'; ab.setAttribute('aria-label', `„${a.titel}" abbrechen`); ab.addEventListener('click', () => abbruchOeffnen(a)); schritte.append(ab); } if (schritte.childElementCount) k.append(schritte); /* Rückmeldungen ("Aufgaben & Feedback" aus dem Konzept). Der Knopf zeigt gleich die Anzahl -- so sieht man ohne Aufklappen, wo etwas besprochen wurde. */ const rueck = el('button', 'karte__rueck'); rueck.type = 'button'; rueck.dataset.hat = a.notizen ? 'ja' : 'nein'; const blase = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); blase.setAttribute('viewBox', '0 0 24 24'); blase.setAttribute('class', 'karte__blase'); blase.setAttribute('aria-hidden', 'true'); blase.innerHTML = ''; rueck.append(blase, el('span', null, a.notizen ? `${a.notizen} ${a.notizen === 1 ? 'Rückmeldung' : 'Rückmeldungen'}` : 'Rückmeldung schreiben')); rueck.addEventListener('click', () => rueckmeldungen(k, a, rueck)); k.append(rueck); /* WER HAT SIE, UND WIE STEHT SIE BEI IHM (21.09.2026). Der Block kommt aus zuteilung.js und nicht von hier: Diese Datei hat 1900 Zeilen, und alles zur Zuteilung gehoert an EINE Stelle. Er gibt null zurueck, wenn niemandem etwas zugeteilt ist -- dann sieht die Karte aus wie vorher. */ if (window.Zuteilung) { const zb = window.Zuteilung.karteBlock(a, ich, () => laden()); if (zb) k.append(zb); } return k; } /* ---------- Abbrechen ---------------------------------------------------- Wunsch 05.09.2026: *"ich will dass man in jedem status die aufgaben auch abbrechen kann."* MIT PFLICHT-GRUND. Ohne ihn weiss in vier Wochen niemand mehr, warum etwas wegfiel -- und dann ist ein Abbruch von einem Loeschen nicht mehr zu unterscheiden. Der Grund steht spaeter an der Aufgabe, zusammen mit dem Namen dessen, der abgebrochen hat. Ein und kein confirm(): confirm blockiert die ganze Seite, nimmt kein Textfeld auf und sieht auf jedem Rechner anders aus. */ function abbruchOeffnen(a) { const dlg = $('abbruch-dialog'); if (!dlg) return; dlg.dataset.id = String(a.id); $('abbruch-titel').textContent = `„${a.titel}" abbrechen?`; /* Sagt, was gleich passiert -- und was NICHT passiert. Die Sorge bei so einem Knopf ist immer, ob etwas verloren geht. */ $('abbruch-was').textContent = `Die Aufgabe steht danach unter „Abgebrochen" ` + `statt in der Spalte ${STATUS_NAME[a.status] || a.status}. ` + `Sie wird nicht gelöscht und lässt sich jederzeit wieder aufnehmen.`; $('abbruch-grund').value = ''; $('abbruch-fehler').textContent = ''; dlg.showModal(); $('abbruch-grund').focus(); } async function abbruchSenden(e) { e.preventDefault(); const dlg = $('abbruch-dialog'); const id = Number(dlg.dataset.id); const grund = $('abbruch-grund').value.trim(); if (grund.length < 3) { $('abbruch-fehler').textContent = 'Bitte kurz sagen, warum.'; $('abbruch-grund').focus(); return; } const knopf = $('abbruch-ok'); knopf.disabled = true; try { const antwort = await hole(`/workspace/api/aufgaben/${id}/abbrechen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ grund }), }); if (!antwort.ok) { const d = await antwort.json().catch(() => ({})); $('abbruch-fehler').textContent = sagWas(d.fehler, 'Hat nicht geklappt.'); return; } /* Gespeichert -- die Verwurfwache darf hier nicht fragen. */ dlg.vergessen?.(); dlg.close(); await laden(); } catch { /* umgeleitet */ } finally { knopf.disabled = false; } } async function wiederAufnehmen(a, b) { b.disabled = true; melde(''); try { const antwort = await hole(`/workspace/api/aufgaben/${a.id}/wiederaufnehmen`, { method: 'POST' }); if (!antwort.ok) { const d = await antwort.json().catch(() => ({})); melde(sagWas(d.fehler, 'Hat nicht geklappt.')); b.disabled = false; return; } await laden(); } catch { b.disabled = false; } } /** Der Bereich UNTER dem Brett -- zugeklappt, mit Anzahl im Titel. * * Bewusst keine fuenfte Spalte: Am Handy werden dadurch alle fuenf * so schmal, dass keine mehr lesbar ist. Und abgebrochene Aufgaben * sind nichts, was man taeglich sieht -- aber sie duerfen auch nicht * verschwinden, sonst waere es Loeschen mit Zwischenschritt. */ function abgebrocheneZeigen(alle) { const kasten = $('abgebrochen'); if (!kasten) return; const eigene = alle.filter((a) => a.status === 'abgebrochen'); if (!eigene.length) { kasten.hidden = true; return; } kasten.hidden = false; $('abgebrochen-zahl').textContent = String(eigene.length); const liste = $('abgebrochen-liste'); liste.textContent = ''; for (const a of eigene) { const k = el('article', 'abbruch-karte'); const kopf = el('div', 'abbruch-karte__kopf'); kopf.append(el('h3', 'abbruch-karte__titel', a.titel)); /* Wo es stand, als es abgebrochen wurde. "Im Review abgebrochen" ist eine andere Aussage als "nie angefangen". */ if (a.status_vorher) { kopf.append(el('span', 'abbruch-karte__vorher', STATUS_NAME[a.status_vorher] || a.status_vorher)); } k.append(kopf); if (a.abbruch_grund) k.append(el('p', 'abbruch-karte__grund', a.abbruch_grund)); const wer = []; if (a.abbruch_von_name) wer.push(a.abbruch_von_name); if (a.abgebrochen_am) wer.push(datumHuebsch(a.abgebrochen_am.slice(0, 10))); if (wer.length) k.append(el('p', 'abbruch-karte__wer', wer.join(' · '))); if (DARF_ABBRECHEN.has(ich.rolle)) { const zurueck = el('button', 'schritt', 'wieder aufnehmen ↩'); zurueck.type = 'button'; zurueck.setAttribute('aria-label', `„${a.titel}" wieder aufnehmen`); zurueck.addEventListener('click', () => wiederAufnehmen(a, zurueck)); k.append(zurueck); } liste.append(k); } } const beschriftung = (status) => ({ offen: 'starten ▶', arbeit: 'zur Freigabe ▶', review: 'erledigt ▶', })[status]; function knopf(text, aufgabe, neuerStatus) { const b = el('button', 'schritt', text); b.type = 'button'; b.addEventListener('click', async () => { b.disabled = true; melde(''); try { const antwort = await hole(`/workspace/api/aufgaben/${aufgabe.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: neuerStatus }), }); if (!antwort.ok) { const d = await antwort.json().catch(() => ({})); melde(d.fehler === 'nicht_erlaubt' ? 'Diese Aufgabe darfst du nicht ändern.' : 'Ändern hat nicht geklappt.'); b.disabled = false; return; } await laden(); } catch { b.disabled = false; } }); return b; } /* ---------- Die Leiste ueber dem Brett ----------------------------------- Wunsch vom 01.09.2026: "verbessere die Aufgabenseite auch bei Manager, Scout und Creator. Die Scouts und Manager sollen vielleicht ein bisschen mehr Optionen haben, wie ihre zugeteilten Creator. Aber auch die Creator sollen es besser und detaillierter sehen." Drei Dinge, und die Reihenfolge ist die Aussage: 1. WAS BRENNT. Ein Satz beim Reinkommen, ob etwas ueberfaellig ist. Ein Brett mit vier Spalten beantwortet das nicht -- man muss vier Spalten durchsehen, um zu wissen, ob man ruhig anfangen kann. 2. AUSSCHNITTE. "Nur meine", "Heute faellig", "Ueberfaellig" -- die Fragen, die man wirklich hat. Sie filtern das Brett, statt eine zweite Liste aufzumachen: Vier Spalten nebeneinander sind der Wert dieser Seite, den nimmt man nicht weg. 3. DIE CREATOR, nur fuer Betreuer. Wer drei betreut, will sehen, was bei einem davon liegt -- ohne die anderen zwei dazwischen. Ein Creator bekommt diese Reihe nicht: Er hat nur sich, und ein Filter mit einem einzigen Eintrag ist ein Knopf, der nichts tut. Alles hier ist ANSICHT. Wer was sehen darf, hat der Server entschieden, bevor eine einzige Aufgabe hier ankommt. */ let ausschnitt = ''; /* '', 'meine', 'faellig', 'heute' */ let nurCreator = 0; /* 0 = alle */ /* DIE LEUTE ALS DATEN, nicht nur als