Files
dogfather-universe/workspace/assets/js/aufgaben.js
T
DogFatherGitandClaude Opus 5 da49a227f1 Aufgabenbrett fuer alle Rollen -- und nur DogFather sieht alles
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. ABER NUR DIE ROLLE DOGFATHER
SOLL WIRKLICH WEITERHIN ALLEINE ALLES SEHEN KOENNEN."
(Nachgereicht: "die Seite Aufgabe auch bei DogFather machen bitte.")

ZWEI FEHLER IN DEN RECHTEN, die beim Nachsehen herausfielen:

  1. Ein MANAGER sah auf dem Aufgabenbrett GAR NICHTS. Die Regel kannte
     nur admin, creator und scout; er fiel in den Zweig "unbekannte
     Rolle sieht nichts". Ein leeres Brett sieht aus wie "nichts zu
     tun", nicht wie ein Fehler -- deshalb ist das lange niemandem
     aufgefallen.

  2. In Kalender, Dateien, Bereichen und Content sah derselbe Manager
     dagegen ALLES (istLeitung). Zwei entgegengesetzte Antworten auf
     dieselbe Frage, in einem Programm.

Jetzt gilt ueberall dasselbe: NUR DogFather sieht alles. Manager und
Scout sehen ihre eigenen Sachen plus die Creator, die ihnen zugeteilt
sind. Dafuer arbeitet betreuteIds() jetzt auch fuer Manager -- die
Datenbank konnte das laengst (in `betreuung` steht eine beliebige
Person), nur diese eine Funktion hat alle ausser Scouts abgewiesen.

GEAENDERT WURDE NUR, WER WAS SIEHT. Was ein Manager DARF -- freigeben,
aendern, Personen verwalten -- haengt weiterhin an istLeitung und ist
unberuehrt. Ohne diese Trennung haette ein Wunsch nach weniger Sicht
stillschweigend die halben Rechte mitgenommen; die Pruefung haelt beides
ausdruecklich fest.

DIE SEITE SELBST bekommt drei Zeilen ueber dem Brett, und die
Reihenfolge ist die Aussage:
  1. WAS BRENNT -- ein Satz beim Reinkommen. Ein Brett aus vier Spalten
     beantwortet das nicht; man muesste alle vier durchsehen, um zu
     wissen, dass nichts brennt.
  2. AUSSCHNITTE -- "Nur meine", "Heute faellig", "Ueberfaellig", jeder
     mit seiner Zahl am Knopf. Ein Filter, der sich erst nach dem Klick
     als leer herausstellt, kostet zweimal Aufmerksamkeit. Sie filtern
     das Brett, statt eine zweite Liste aufzumachen: Vier Spalten
     nebeneinander sind der Wert dieser Seite.
  3. DIE CREATOR -- fuer Betreuer und DogFather, je mit offener Anzahl
     und einer Warnzahl fuer Ueberfaelliges. Bei DogFather heisst die
     Reihe "Alle Creator", sonst "Deine Creator": "deine" waere bei ihm
     eine falsche Auskunft.

Ein Creator bekommt weder "Nur meine" noch die Creator-Reihe -- bei ihm
ist ohnehin alles seins, und ein Filter mit einem einzigen Eintrag ist
ein Knopf, der nichts tut. Die Liste der Creator stammt aus den
Aufgaben selbst und nicht aus einer Personenabfrage: Dann stehen dort
genau die, die man ohnehin sehen darf, und niemals einer mehr.

NEBENBEI (Screen 1): In der Betreuer-Auswahl stand "Dogfather
(DogFather)" -- zweimal dasselbe Wort, nur anders geschrieben. Die
Klammer entfaellt jetzt, wenn sie dasselbe sagt wie der Name.

pruef-aufgabenbrett.mjs prueft die ABGRENZUNG zuerst und an konkreten
Aufgaben, deren Titel verraten, wem sie gehoeren -- eine huebschere
Filterleiste ist eine Annehmlichkeit, eine Rechteregel, die zu viel
zeigt, ist ein Schaden. Dazu eine Gegenprobe zur alten Regel (der
Manager sieht ueberhaupt etwas) und die ausdrueckliche Bestaetigung,
dass er weiterhin anlegen darf.

Ein Fehler steckte in der Pruefung selbst: Sie erwartete beim Creator
eine feste Zahl und haing damit von ihrer eigenen Reihenfolge ab -- der
Abschnitt davor legt eine weitere Aufgabe an. Sie vergleicht jetzt gegen
die Schnittstelle. Das ist ohnehin die bessere Frage: nicht "sind es
drei", sondern "verschluckt die Seite etwas".

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

615 lines
23 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.
/* ===================================================================
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. */
const LEITUNG = new Set(['admin', 'manager']);
/* `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 Review-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.' },
{ status: 'review', name: 'Review', unter: 'Wartet auf Freigabe',
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' };
const $ = (id) => document.getElementById(id);
const heute = new Date().toISOString().slice(0, 10);
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 (!confirm('Diese Rückmeldung 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((await r.json().catch(() => ({}))).fehler || 'Ging nicht.'); return; }
await laden();
} catch { /* umgeleitet */ } finally { senden.disabled = false; }
});
box.append(form);
feld.focus();
}
function karte(a) {
const k = el('article', 'karte');
k.dataset.prio = a.prioritaet;
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);
if (a.beschreibung) k.append(el('p', 'karte__text', a.beschreibung));
const zeile = el('div', 'karte__zeile');
if (a.frist) {
const faellig = a.status !== 'erledigt' && a.frist < heute;
const f = el('span', 'karte__frist' + (faellig ? ' karte__frist--faellig' : ''),
(faellig ? 'überfällig ' : 'bis ') + datumHuebsch(a.frist));
zeile.append(f);
}
if (a.verantwortlich_name) zeile.append(el('span', null, '· ' + a.verantwortlich_name));
if (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');
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]));
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 = '<path d="M20.4 12.4c0 3.8-3.8 6.9-8.4 6.9-1 0-2-.15-2.9-.42L4 20.4l1.6-3.6a6.6 6.6 0 0 1-2-4.4C3.6 8.6 7.4 5.5 12 5.5s8.4 3.1 8.4 6.9z"/>';
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);
return k;
}
const beschriftung = (status) => ({
offen: 'starten ▶', arbeit: 'zum Review ▶', 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 */
let alleAufgaben = [];
const istOffen = (a) => a.status !== 'erledigt';
const istFaellig = (a) => istOffen(a) && a.frist && a.frist < heute;
const istHeute = (a) => istOffen(a) && a.frist === heute;
const istMeine = (a) => a.verantwortlich_id === ich.id;
function passt(a) {
if (nurCreator && a.creator_id !== nurCreator) return false;
if (ausschnitt === 'meine') return istMeine(a);
if (ausschnitt === 'faellig') return istFaellig(a);
if (ausschnitt === 'heute') return istHeute(a);
return true;
}
function lageZeigen(alle) {
const ziel = $('lage');
if (!ziel) return;
const faellig = alle.filter(istFaellig).length;
const heuteN = alle.filter(istHeute).length;
const offen = alle.filter(istOffen).length;
const meine = alle.filter((a) => istOffen(a) && istMeine(a)).length;
ziel.textContent = '';
let satz, stufe;
if (faellig) {
satz = `${faellig} ${faellig === 1 ? 'Aufgabe ist' : 'Aufgaben sind'} überfällig – das zuerst.`;
stufe = 'warnung';
} else if (heuteN) {
satz = `${heuteN} ${heuteN === 1 ? 'Aufgabe ist' : 'Aufgaben sind'} heute fällig.`;
stufe = 'offen';
} else if (offen) {
satz = `${offen} ${offen === 1 ? 'Aufgabe steht' : 'Aufgaben stehen'} an, nichts davon brennt.`;
stufe = 'offen';
} else {
satz = 'Nichts offen – alles abgearbeitet.';
stufe = 'ruhig';
}
ziel.dataset.stufe = stufe;
ziel.append(el('span', 'lage__satz', satz));
/* Die eigene Zahl steht daneben, nicht im Satz: Auf einem Brett, das
auch fremde Aufgaben zeigt, ist "wie viel davon ist meins" die
zweite Frage -- aber eben die zweite. */
if (ich.rolle !== 'creator' && meine) {
ziel.append(el('span', 'lage__meins', `${meine} davon bei dir`));
}
ziel.hidden = false;
}
function ausschnitteBauen(alle) {
const ziel = $('ausschnitte');
if (!ziel) return;
ziel.textContent = '';
const knopf = (wert, name, anzahl) => {
const b = el('button', 'schnitt');
b.type = 'button';
b.dataset.wert = wert;
b.setAttribute('aria-pressed', String(ausschnitt === wert));
b.append(el('span', null, name));
/* Die Zahl steht am Knopf. Ein Filter, der sich erst nach dem
Klick als leer herausstellt, kostet zweimal Aufmerksamkeit. */
b.append(el('span', 'schnitt__zahl', String(anzahl)));
if (!anzahl && wert) b.disabled = true;
b.addEventListener('click', () => {
ausschnitt = ausschnitt === wert ? '' : wert;
neuZeichnen();
});
return b;
};
ziel.append(knopf('', 'Alle', alle.length));
if (ich.rolle !== 'creator') {
ziel.append(knopf('meine', 'Nur meine',
alle.filter((a) => istOffen(a) && istMeine(a)).length));
}
ziel.append(knopf('heute', 'Heute fällig', alle.filter(istHeute).length));
const f = knopf('faellig', 'Überfällig', alle.filter(istFaellig).length);
f.dataset.warn = 'ja';
ziel.append(f);
ziel.hidden = false;
}
function creatorReiheBauen(alle) {
const ziel = $('creator-reihe');
if (!ziel) return;
ziel.textContent = '';
if (ich.rolle === 'creator') { ziel.hidden = true; return; }
/* Die Liste kommt aus den Aufgaben selbst, nicht aus einer
Personenabfrage: Dann stehen dort genau die Creator, die man
ohnehin sehen darf -- und niemals einer mehr. */
const nach = new Map();
for (const a of alle) {
if (!a.creator_id || !a.creator_name) continue;
const w = nach.get(a.creator_id) || { name: a.creator_name, offen: 0, faellig: 0 };
if (istOffen(a)) w.offen++;
if (istFaellig(a)) w.faellig++;
nach.set(a.creator_id, w);
}
/* Unter zwei Creatorn ist die Reihe eine Zeile ohne Auswahl. */
if (nach.size < 2) { ziel.hidden = true; return; }
/* "Deine Creator" stimmt fuer Scout und Manager -- bei DogFather
waeren es ALLE, und "deine" waere dort eine falsche Auskunft. */
ziel.append(el('span', 'creator-reihe__schild',
ich.rolle === 'admin' ? 'Alle Creator' : 'Deine Creator'));
const knopf = (id, name, offen, faellig) => {
const b = el('button', 'creator-knopf');
b.type = 'button';
b.setAttribute('aria-pressed', String(nurCreator === id));
b.append(el('span', 'creator-knopf__name', name));
b.append(el('span', 'creator-knopf__zahl', String(offen)));
if (faellig) {
const m = el('span', 'creator-knopf__warn', String(faellig));
m.title = `${faellig} überfällig`;
b.append(m);
}
b.addEventListener('click', () => {
nurCreator = nurCreator === id ? 0 : id;
neuZeichnen();
});
return b;
};
ziel.append(knopf(0, 'Alle', alle.filter(istOffen).length, alle.filter(istFaellig).length));
for (const [id, w] of [...nach.entries()].sort((a, b) => b[1].offen - a[1].offen)) {
ziel.append(knopf(id, w.name, w.offen, w.faellig));
}
ziel.hidden = false;
}
function neuZeichnen() {
ausschnitteBauen(alleAufgaben);
creatorReiheBauen(alleAufgaben);
zeichne(alleAufgaben.filter(passt));
}
function zeichne(aufgaben) {
const brett = $('brett');
brett.textContent = '';
brett.setAttribute('aria-busy', 'false');
for (const s of SPALTEN) {
const eigene = aufgaben.filter((a) => a.status === s.status);
const spalte = el('section', 'spalte');
spalte.dataset.status = s.status;
/* Die Karte merkt sich, wozu sie gehoert -- daraus findet der
Sprung vom Bericht die richtigen wieder, ohne die Liste ein
zweites Mal durchzugehen. */
const kopf = el('h2', 'spalte__kopf');
kopf.append(el('span', 'spalte__punkt'), el('span', null, s.name),
el('span', 'spalte__zahl', String(eigene.length)));
spalte.append(kopf);
/* Was diese Spalte bedeutet -- steht immer da, nicht nur wenn sie
leer ist. "Review" allein sagt einem Neuen nichts. */
spalte.append(el('p', 'spalte__unter', s.unter));
if (!eigene.length) spalte.append(el('p', 'spalte__leer', s.leer));
else for (const a of eigene) {
const k = karte(a);
k.dataset.status = a.status;
if (a.status !== 'erledigt' && a.frist && a.frist < heute) k.dataset.faellig = 'ja';
spalte.append(k);
}
brett.append(spalte);
}
}
/* ---------- Sprung vom Bericht -------------------------------------------
"wenn man auf die Kisten drueckt, sofort zu diesem Punkt
weitergeschickt werden" (01.09.2026).
Hier wird NICHT gefiltert, sondern hervorgehoben. Das Brett lebt
davon, dass man alle vier Spalten nebeneinander sieht -- wer nur die
ueberfaelligen zeigt, nimmt genau den Ueberblick weg, wegen dem man
hergekommen ist. */
const ZEIG_TEXT = {
offen: 'Aufgaben, die offen sind',
arbeit: 'Aufgaben, die gerade laufen',
review: 'Aufgaben, die auf Freigabe warten',
erledigt: 'Aufgaben, die erledigt sind',
ueberfaellig: 'Aufgaben, deren Frist vorbei ist',
};
function sprungZeigen() {
const was = window.zielSchluessel?.();
if (!was || !ZEIG_TEXT[was]) return;
const treffer = was === 'ueberfaellig'
? document.querySelectorAll('.karte[data-faellig="ja"]')
: document.querySelectorAll(`.karte[data-status="${was}"]`);
window.zielMarkieren?.(treffer, ZEIG_TEXT[was]);
}
async function laden() {
try {
const antwort = await hole('/workspace/api/aufgaben');
if (!antwort.ok) { melde('Aufgaben konnten nicht geladen werden.'); return; }
alleAufgaben = (await antwort.json()).aufgaben || [];
lageZeigen(alleAufgaben);
neuZeichnen();
/* Erst NACH dem Zeichnen -- vorher gibt es die Karten nicht, und
der Sprung ginge ins Leere, ohne dass jemand etwas merkt. Nur
beim ersten Laden: Sonst springt die Seite nach jeder
Statusaenderung erneut weg. */
if (!gesprungen) { gesprungen = true; sprungZeigen(); }
} catch { /* bereits umgeleitet */ }
}
/* ---------- Neue Aufgabe -------------------------------------------- */
function formularUmschalten(auf) {
$('neu').hidden = !auf;
$('neu-oeffnen').hidden = auf;
if (auf) $('f-titel').focus();
}
$('neu-oeffnen').addEventListener('click', () => formularUmschalten(true));
$('neu-abbrechen').addEventListener('click', () => {
$('neu').reset(); $('neu-fehler').textContent = ''; formularUmschalten(false);
});
$('neu').addEventListener('submit', async (e) => {
e.preventDefault();
const knopfEl = $('neu-speichern');
$('neu-fehler').textContent = '';
const daten = {
titel: $('f-titel').value,
beschreibung: $('f-text').value,
prioritaet: $('f-prio').value,
frist: $('f-frist').value,
};
if (LEITUNG.has(ich.rolle)) {
daten.creator_id = $('f-creator').value || null;
daten.verantwortlich_id = $('f-verant').value || null;
}
knopfEl.disabled = true;
try {
const antwort = await hole('/workspace/api/aufgaben', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(daten),
});
if (!antwort.ok) {
const d = await antwort.json().catch(() => ({}));
$('neu-fehler').textContent = d.fehler || 'Anlegen hat nicht geklappt.';
return;
}
$('neu').reset();
formularUmschalten(false);
await laden();
} catch { /* bereits umgeleitet */ } finally { knopfEl.disabled = false; }
});
/* ---------- Bearbeiten ------------------------------------------------ */
const dialog = $('bearbeiten-dialog');
let bearbeitet = null;
function dialogOeffnen(a) {
bearbeitet = a;
$('b-titel').value = a.titel;
$('b-text').value = a.beschreibung || '';
$('b-prio').value = a.prioritaet;
$('b-frist').value = a.frist || '';
$('b-fehler').textContent = '';
if (LEITUNG.has(ich.rolle)) {
$('b-creator').value = a.creator_id ? String(a.creator_id) : '';
$('b-verant').value = a.verantwortlich_id ? String(a.verantwortlich_id) : '';
$('b-loeschen').hidden = false;
}
dialog.showModal();
$('b-titel').focus();
}
$('b-abbrechen').addEventListener('click', () => dialog.close());
$('b-speichern').addEventListener('click', async () => {
const daten = {
titel: $('b-titel').value,
beschreibung: $('b-text').value,
prioritaet: $('b-prio').value,
frist: $('b-frist').value,
};
if (LEITUNG.has(ich.rolle)) {
daten.creator_id = $('b-creator').value || null;
daten.verantwortlich_id = $('b-verant').value || null;
}
$('b-speichern').disabled = true;
try {
const a = await hole(`/workspace/api/aufgaben/${bearbeitet.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(daten),
});
if (!a.ok) {
$('b-fehler').textContent = (await a.json().catch(() => ({}))).fehler || 'Speichern ging nicht.';
return;
}
dialog.close();
await laden();
} catch { /* umgeleitet */ } finally { $('b-speichern').disabled = false; }
});
$('b-loeschen').addEventListener('click', async () => {
if (!confirm(`Aufgabe wirklich loeschen?
"${bearbeitet.titel}"
Das laesst sich nicht rueckgaengig machen. Erledigte Aufgaben sollten normalerweise stehen bleiben.`)) return;
try {
const a = await hole(`/workspace/api/aufgaben/${bearbeitet.id}`, { method: 'DELETE' });
if (!a.ok) { $('b-fehler').textContent = 'Loeschen ging nicht.'; return; }
dialog.close();
await laden();
} catch { /* umgeleitet */ }
});
/* ---------- Start ----------------------------------------------------- */
(async () => {
try {
const antwort = await hole('/workspace/api/ich');
if (!antwort.ok) { location.assign('/workspace/'); return; }
ich = await antwort.json();
} catch { return; }
window.werZeigen?.(ich);
if (LEITUNG.has(ich.rolle)) {
$('feld-creator').hidden = false;
$('feld-verant').hidden = false;
$('b-feld-creator').hidden = false;
$('b-feld-verant').hidden = false;
try {
const { personen } = await (await hole('/workspace/api/personen')).json();
for (const p of personen) {
for (const ziel of ['f-creator', 'f-verant', 'b-creator', 'b-verant']) {
const o = el('option', null, `${p.name} (${p.rolle})`);
o.value = String(p.id);
$(ziel).append(o);
}
}
} catch { /* Zuordnung bleibt dann leer -- kein Grund abzubrechen */ }
}
await laden();
})();
})();