Files
dogfather-universe/workspace/assets/js/zuteilung.js
T
DogFatherGitandClaude Opus 5 ef911691b7 Der zweite Weg ist weg -- nicht nur sein Knopf
VanVan im Support, Meldung #8: „Wenn man auf ich fange an drueckt
steht dort in Bearbeitung und wenn man auf fertig drueckt dann wird
es zu erledigt. DIE AUFGABE BLEIBT ABER IM STATUS OFFEN STEHEN."

GESTERN HABE ICH DIE HALBE ARBEIT GEMACHT und daneben eine Ausrede
geschrieben. Die zwei Knoepfe kamen weg, und in den Kommentar kam:

    „Der Weg `/mein-stand` bleibt bestehen -- er ist die Schranke,
     falls ihn jemand direkt anspricht."

Eine Route ist keine Schranke gegen sich selbst. Sie setzte
weiterhin NUR `aufgaben_zuteilung.zustand` und liess
`aufgaben.status` stehen -- also genau den Widerspruch, den VanVan
beschrieben hat. Ich hatte ihn unsichtbar gemacht, nicht
abgeschafft: kein Knopf mehr, das Verhalten unveraendert im System.

GEFUNDEN BEIM NACHMESSEN AM LAUFENDEN SERVER, nicht beim Schreiben.
Filipe hat auf den Screenshot gezeigt und gesagt, es sei noch nicht
in Ordnung. Statt meine Pruefungen zu zitieren habe ich die Route
gelesen -- und dort stand es.

NACHGEMESSEN, BEVOR SIE WEGKAM: Kein einziger Aufruf mehr im
ausgelieferten Browsercode (grep ueber alle JS- und HTML-Dateien des
Workspace). Nur zwei Pruefungen benutzten sie.

UND EINE DAVON NICKTE DEN FEHLER AB. In pruef-zuteilung stand:

    Bea setzt "in Bearbeitung" (HTTP 200)
    und danach "erledigt" (HTTP 200)

Zwei gruene Haken ueber genau dem Verhalten, das gemeldet wurde --
weil sie nur den Rueckgabewert ansahen und nie den Aufgabenstatus
daneben. Eine Pruefung, die nur eine Haelfte misst, kann den
Widerspruch gar nicht finden. Jetzt steht dort:

    Bea setzt "in Bearbeitung" (HTTP 200)
      und BEIDES steht auf "in Arbeit" (Aufgabe arbeit,
      Zuteilung arbeit) — das war VanVans Befund
    und danach "erledigt" (HTTP 200)
      und wieder beides (Aufgabe erledigt, Zuteilung erledigt)

WAS JETZT GILT: `PATCH /workspace/api/aufgaben/:id` mit `{ status }`.
Er setzt den Status UND zieht die Zuteilung mit
(`zuteilungenNachStatus`), kennt dieselbe Sperre fuer dauerhafte
Aufgaben und dieselbe Rechtepruefung. Eine Frage, eine Antwort.

ENTFERNT STATT AUSKOMMENTIERT -- dieselbe Entscheidung wie bei
`/vorlagen/hilfe` am 01.09.: Eine Route, die niemand mehr aufruft,
wird beim naechsten Mal fuer lebenden Code gehalten und mitgepflegt.

EIN SCHRECKMOMENT UNTERWEGS, der sich als Messfehler herausstellte:
Nach der Umstellung meldete pruef-bewerbung-aufgaben eine 404 beim
Abhaken -- also der Verdacht, dass eine ZUGETEILTE Aufgabe ueber den
Statusweg gar nicht erreichbar ist und ich gerade etwas kaputt
gemacht haette. Nachgemessen statt geglaubt: Die Aufgabe steht in
ihrer Liste, der PATCH antwortet 200. Die rote Zeile war eine
DRITTE Stelle, die ich beim Umstellen uebersehen hatte und die noch
auf die alte Route zeigte. Zwei Minuten Messung statt einer Stunde
Suche an der falschen Stelle.

GEPRUEFT

  pruef-zuteilung             ok, mit zwei neuen Zeilen, die BEIDE
                              Zustaende messen
    den zweiten Weg (mein-stand) gibt es nicht mehr (404)
  pruef-bewerbung-aufgaben    178 ok
  pruef-struktur              102 ok, 414 -> 413 Routen
  pruef-aufgabenbrett · pruef-modi-katalog 159 ·
  pruef-aufgaben-vorlagen 60

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-03 11:31:18 +02:00

588 lines
27 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.
/* =====================================================================
zuteilung.js — WER HAT DIE AUFGABE, UND WIE STEHT SIE BEI IHM
=====================================================================
Auftrag vom 21.09.2026, Abschnitte 1 bis 5. Die Oberfläche dazu.
EIN EIGENES MODUL, weil aufgaben.js schon 1900 Zeilen hat. Alles,
was mit Zuteilung zu tun hat, steht hier an einer Stelle; aufgaben.js
ruft an genau zwei Stellen hierher.
WAS ES BAUT:
Band oben Für einen Modi sein persönliches Resümee, für
DogFather und die Hände die Übersicht über alle.
An der Karte Wer sie hat, wie es bei jedem steht, und die
Knöpfe, die für DIESEN Menschen gerade sinnvoll
sind.
DIE WICHTIGSTE REGEL DARIN: Es wird nur angeboten, was auch geht.
Ein "Annehmen" an einer Aufgabe, die man nicht bekommen hat, wäre
ein Knopf, der mit 403 antwortet — und die Meldung erscheint oben
auf der Seite, wo man sie bei einer Karte weiter unten gar nicht
sieht. Diese Lektion steht in aufgaben.js schon einmal; sie gilt
hier genauso.
DIE TEXTE STEHEN NICHT HIER, SONDERN KOMMEN VOM SERVER
(`zustand_text`, `bewertung_text_kurz`). Zwei Listen derselben
Wörter laufen auseinander, und dann heißt derselbe Zustand an zwei
Stellen anders. */
(function () {
'use strict';
var el = function (was, klasse, text) {
var e = document.createElement(was);
if (klasse) e.className = klasse;
if (text !== undefined) e.textContent = text;
return e;
};
/* Damit die Meldung dort steht, wo der Mensch hinsieht, und nicht
ganz oben auf der Seite. */
function sagen(wohin, text, gut) {
if (!wohin) return;
wohin.textContent = text;
wohin.dataset.gut = gut ? 'ja' : 'nein';
wohin.hidden = false;
}
async function schicken(pfad, koerper) {
var a = await fetch(pfad, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(koerper || {}),
});
var j = null;
try { j = await a.json(); } catch (e) { /* leere Antwort */ }
return { ok: a.ok, code: a.status, json: j || {} };
}
/* =====================================================================
DAS BAND OBEN
===================================================================== */
/** Eine Zahl mit Wort darunter. Die Zahl groß, damit man sie von
* oben erfasst, ohne zu lesen. */
function zahlKachel(wert, wort, art) {
var k = el('div', 'zk' + (art ? ' zk--' + art : ''));
k.append(el('span', 'zk__zahl', String(wert)));
k.append(el('span', 'zk__wort', wort));
return k;
}
/** Das persönliche Resümee eines Menschen. */
function meinBand(r) {
var kasten = el('section', 'z-band');
var kopf = el('div', 'z-band__kopf');
kopf.append(el('h2', 'z-band__titel', 'Deine Aufgaben'));
/* EIN SATZ, DER DIE ZAHLEN EINORDNET. Fünf nackte Zahlen sind
eine Tabelle; ein Satz ist eine Auskunft. */
var satz = r.dran === 0
? (r.gesamt === 0
? 'Dir ist im Moment nichts zugeteilt.'
: 'Alles erledigt — nichts offen.')
: (r.ueberfaellig > 0
? r.dran + (r.dran === 1 ? ' Aufgabe ist dran' : ' Aufgaben sind dran')
+ ', ' + r.ueberfaellig + ' davon überfällig.'
: r.dran + (r.dran === 1 ? ' Aufgabe ist dran.' : ' Aufgaben sind dran.'));
kopf.append(el('p', 'z-band__satz', satz));
kasten.append(kopf);
var reihe = el('div', 'z-band__zahlen');
reihe.append(zahlKachel(r.offen, 'noch offen', r.offen > 0 ? 'wach' : null));
reihe.append(zahlKachel(r.angenommen + r.arbeit, 'in Arbeit'));
reihe.append(zahlKachel(r.erledigt, 'erledigt', 'gut'));
if (r.ueberfaellig > 0) reihe.append(zahlKachel(r.ueberfaellig, 'überfällig', 'warn'));
if (r.gelobt > 0) reihe.append(zahlKachel(r.gelobt, 'gut gemacht', 'gut'));
if (r.zu_verbessern > 0) reihe.append(zahlKachel(r.zu_verbessern, 'mit Rückmeldung', 'wach'));
kasten.append(reihe);
return kasten;
}
/** Die Übersicht über alle — für DogFather und die Hände. */
function teamBand(daten) {
var kasten = el('section', 'z-band z-band--team');
var kopf = el('div', 'z-band__kopf');
kopf.append(el('h2', 'z-band__titel', 'Wer hat gerade was'));
kopf.append(el('p', 'z-band__satz',
/* "Eine Zeile je Person" stimmte, solange es Zeilen waren. Seit
dem 23.09.2026 stehen die Leute nebeneinander -- der Satz
beschrieb also eine Anordnung, die es nicht mehr gibt. Ein
Hinweistext, der etwas anderes sagt als der Bildschirm zeigt,
ist schlimmer als keiner: Man sucht dann nach Zeilen. */
'Tippe jemanden an, um nur seine Aufgaben zu sehen.'));
kasten.append(kopf);
var leute = daten.leute || [];
if (!leute.length) {
kasten.append(el('p', 'z-leer', 'Es gibt noch niemanden im Team.'));
return kasten;
}
var liste = el('div', 'z-leute');
leute.forEach(function (p) {
var z = el('button', 'z-person');
z.type = 'button';
z.dataset.person = String(p.id);
z.append(el('span', 'z-person__name', p.name));
var m = el('span', 'z-person__marken');
/* NUR WAS ETWAS SAGT. Eine Null neben jeder Person ist Rauschen;
wer nichts offen hat, soll das auch als Ruhe sehen. */
if (p.offen) m.append(el('span', 'z-marke z-marke--wach', p.offen + ' offen'));
if (p.angenommen + p.arbeit) {
m.append(el('span', 'z-marke', (p.angenommen + p.arbeit) + ' in Arbeit'));
}
if (p.ueberfaellig) m.append(el('span', 'z-marke z-marke--warn', p.ueberfaellig + ' überfällig'));
if (p.abgelehnt) m.append(el('span', 'z-marke z-marke--still', p.abgelehnt + ' abgelehnt'));
if (p.erledigt) m.append(el('span', 'z-marke z-marke--gut', p.erledigt + ' erledigt'));
if (!m.childElementCount) m.append(el('span', 'z-marke z-marke--still', 'nichts zugeteilt'));
z.append(m);
liste.append(z);
});
kasten.append(liste);
return kasten;
}
/* =====================================================================
AN DER KARTE
===================================================================== */
/** Baut den Zuteilungsblock einer Karte — oder gibt null zurück,
* wenn es nichts zu zeigen gibt. */
function karteBlock(a, ich, nachLaden) {
var zu = a.zuteilung || [];
/* BIS ZUM 22.09.2026 STAND HIER `if (!zu.length) return null;`.
Das war richtig, solange es an einer Aufgabe ohne Zuteilung
nichts zu tun gab. Seit es das Bewerben gibt, ist genau die
unverteilte Aufgabe die interessanteste -- und sie haette
keinen Platz fuer den Knopf gehabt.
Leer bleibt der Block trotzdem, wenn wirklich nichts drinsteht:
Am Ende wird geprueft, ob ueberhaupt etwas angehaengt wurde. */
var darfEntscheiden = !!(ich && ich.darf_entscheiden);
var bewerbungen = a.bewerbungen || [];
var kasten = el('div', 'z-karte');
/* (1) WER HAT SIE. Bei "pool" steht statt der Liste, wer sie
genommen hat -- das ist die Auskunft, die zählt. */
if (a.verteilart === 'pool' && a.uebernommen_von) {
kasten.append(el('p', 'z-karte__zeile',
a.uebernommen_von + ' hat sie übernommen'));
} else if (a.verteilart === 'pool') {
/* DER SATZ HING AN EINER REGEL, DIE ES NICHT MEHR GIBT
(22.09.2026). „Wer zuerst Zeit hat" war die Beschreibung des
Pools, solange sich jeder selbst bedienen durfte. Seit Modis
und die linke Hand sich bewerben, stimmt er fuer genau die
Leute nicht mehr, die ihn lesen -- und ein Satz, der etwas
Falsches verspricht, ist schlimmer als keiner.
Er sagt deshalb jedem, was FUER IHN gilt. */
var angeboten = zu.filter(function (z) { return z.zustand !== 'abgelehnt'; }).length;
/* „WER ZUERST ZEIT HAT" GILT WIEDER -- nur zaehlt jetzt, wer
sich zuerst bewirbt. Der Satz stand einen Nachmittag lang
anders da („bewirb dich, wenn du Zeit hast"), und das war
zwar wahr, aber kleiner als das, was gemeint war. */
kasten.append(el('p', 'z-karte__zeile', darfEntscheiden
? 'Frei für ' + angeboten + ' Leute — wer zuerst Zeit hat'
: 'Frei für ' + angeboten + ' Leute — wer zuerst Zeit hat, bewirbt sich zuerst'));
}
var wer = el('ul', 'z-wer');
/* Bewerbungen stehen weiter unten in ihrem eigenen Block -- hier
noch einmal waeren sie dieselbe Sache zweimal. */
zu.filter(function (z) { return z.zustand !== 'beworben'; }).forEach(function (z) {
var zeile = el('li', 'z-wer__zeile');
zeile.dataset.zustand = z.zustand;
zeile.append(el('span', 'z-wer__name', z.person_name || 'Jemand'));
zeile.append(el('span', 'z-wer__stand', z.zustand_text));
/* DER GRUND GEHÖRT DANEBEN, nicht hinter einen zweiten Klick:
Wer "abgelehnt" liest, will als Nächstes wissen, warum. */
if (z.zustand === 'abgelehnt' && z.grund) {
zeile.append(el('span', 'z-wer__grund', '„' + z.grund + '“'));
}
if (z.bewertung) {
var b = el('span', 'z-wer__bewertung', z.bewertung_text_kurz);
b.dataset.art = z.bewertung;
zeile.append(b);
if (z.bewertung_text) zeile.append(el('span', 'z-wer__grund', z.bewertung_text));
}
/* WAS DIE LEITUNG DAZU GESAGT HAT. Bei einer angenommenen oder
abgelehnten Bewerbung ist das die Antwort auf die Bitte --
sie gehoert daneben, nicht hinter einen zweiten Klick. */
if (z.entscheid_text) {
zeile.append(el('span', 'z-wer__grund',
'„' + z.entscheid_text + '“'
+ (z.entschieden_von_name ? ' – ' + z.entschieden_von_name : '')));
}
wer.append(zeile);
});
if (wer.childElementCount) kasten.append(wer);
var meldung = el('p', 'z-meldung');
meldung.hidden = true;
/* (2) MEINE KNÖPFE. Nur, was für MICH gerade geht. */
var mein = a.meine_zuteilung;
var knoepfe = el('div', 'z-knoepfe');
function knopf(text, klasse, tun) {
var b = el('button', 'knopf ' + (klasse || ''), text);
b.type = 'button';
b.addEventListener('click', function () { tun(b); });
knoepfe.append(b);
return b;
}
/* SICH BEWERBEN (22.09.2026).
Filipe: „ich will dass die sich fuer aufgaben bewerben koennen
aber die rechte hand oder dogfather muessen annehmen oder
ablehnen koennen."
DER KNOPF STEHT NUR DA, WO ER AUCH ETWAS BEWIRKT. Drei
Bedingungen, und jede hat einen Grund:
* ich entscheide nicht selbst -- wer entscheidet, traegt
sich ein statt zu bitten
* ich habe sie noch nicht -- sonst nehme ich an oder ab
* sie ist noch frei -- eine Bewerbung auf etwas
Vergebenes ist eine
Enttaeuschung mit Anlauf
Der Text ist freiwillig. Beim ABLEHNEN einer zugeteilten
Aufgabe ist die Begruendung Pflicht -- dort sagt jemand ab, der
gefragt wurde. Hier bittet jemand; ein „bitte" braucht keine
Begruendung. */
var schonWer = a.uebernommen_von
|| zu.some(function (z) {
return z.zustand === 'angenommen' || z.zustand === 'arbeit' || z.zustand === 'erledigt';
});
/* ==== UND NICHT AUF DIE EIGENE AUFGABE (30.09.2026) ==========
VanVan im Support: „Wenn ein Modi sich auf eine Aufgabe beworben
hat und die von uns angenommen wurde, dann steht beim Modi unter
der Aufgabe immer noch ich bewerbe mich."
Die Ursache lag im Server (eine Aufgabe aus der Vorlage bekam
keine Zuteilungszeile, `mein` war deshalb leer, und der erste
Zweig unten lautet `!mein`). Das ist behoben.
DIESE ZEILE IST TROTZDEM RICHTIG, und zwar unabhaengig davon:
Auf eine Aufgabe, die mir schon gehoert, bewirbt man sich nicht.
Sie faengt jeden weiteren Weg ab, der eine Aufgabe ohne
Zuteilungszeile anlegt -- gemessen hatten 8 von 11 Aufgaben mit
Verantwortlichem keine, und nicht alle kamen aus der Vorlage. */
var gehoertMir = !!(ich && a.verantwortlich_id != null
&& a.verantwortlich_id === ich.id);
var kannBewerben = !darfEntscheiden && !a.ich_beworben && !gehoertMir
&& (!mein || mein.zustand === 'abgelehnt'
|| (mein.zustand === 'offen' && a.verteilart === 'pool'))
&& !(a.verteilart === 'pool' && schonWer);
if (a.ich_beworben) {
kasten.append(el('p', 'z-karte__zeile z-karte__zeile--warte',
'Du hast dich beworben — DogFather oder die rechte Hand melden sich.'));
knopf('Bewerbung zurückziehen', '', async function (b) {
b.disabled = true;
var r = await schicken('/workspace/api/aufgaben/' + a.id + '/bewerbung-zurueck');
if (r.ok) { nachLaden(); return; }
b.disabled = false;
sagen(meldung, window.sagWas ? window.sagWas(r.json.fehler) : 'Hat nicht geklappt.', false);
});
} else if (kannBewerben) {
knopf('Ich bewerbe mich', 'knopf--klar', async function () {
var antwort = await window.frageNach({
titel: 'Für diese Aufgabe bewerben?',
was: 'DogFather oder die rechte Hand entscheiden, ob du sie bekommst.',
bleibt: 'Du kannst deine Bewerbung jederzeit wieder zurückziehen.',
grund: 'Willst du etwas dazuschreiben? (freiwillig)',
grundPflicht: false,
platzhalter: 'z. B. ich habe Freitag Zeit dafür',
ja: 'Bewerben', nein: 'Doch nicht',
});
if (!antwort) return;
var r = await schicken('/workspace/api/aufgaben/' + a.id + '/bewerben',
{ text: (antwort && antwort.grund) || '' });
if (r.ok) { nachLaden(); return; }
sagen(meldung, r.code === 409 && r.json.wer
? (r.json.wer + ' war schneller — die Aufgabe ist schon vergeben.')
: (window.sagWas ? window.sagWas(r.json.fehler) : 'Hat nicht geklappt.'), false);
});
}
if (mein && mein.zustand === 'offen') {
if (a.verteilart === 'pool') {
/* NUR WER ENTSCHEIDET, NIMMT SICH SELBST ETWAS. Fuer alle
anderen steht oben der Bewerben-Knopf. */
if (darfEntscheiden) {
knopf('Ich übernehme das', 'knopf--klar', async function (b) {
b.disabled = true;
var r = await schicken('/workspace/api/aufgaben/' + a.id + '/uebernehmen');
if (r.ok) { nachLaden(); return; }
b.disabled = false;
sagen(meldung, r.code === 409 && r.json.wer
? (r.json.wer + ' war schneller — die Aufgabe ist schon vergeben.')
: (window.sagWas ? window.sagWas(r.json.fehler) : 'Hat nicht geklappt.'), false);
});
}
} else {
knopf('Annehmen', 'knopf--klar', async function (b) {
b.disabled = true;
var r = await schicken('/workspace/api/aufgaben/' + a.id + '/annehmen');
if (r.ok) { nachLaden(); return; }
b.disabled = false;
sagen(meldung, window.sagWas ? window.sagWas(r.json.fehler) : 'Hat nicht geklappt.', false);
});
}
/* ABLEHNEN GIBT ES NUR, WO ETWAS ZUGETRAGEN WURDE. Bei einer
Pool-Aufgabe, fuer die man sich bewerben soll, waere „Ablehnen"
eine Absage auf eine Frage, die niemand gestellt hat. */
if (!(a.verteilart === 'pool' && !darfEntscheiden))
knopf('Ablehnen', '', async function () {
/* MIT BEGRÜNDUNG, und sie ist Pflicht -- der Server lehnt
sonst ab. Der Dialog sagt das VOR dem Tippen, statt es
hinterher als Fehler zu melden. */
var antwort = await window.frageNach({
titel: 'Aufgabe ablehnen?',
was: 'Sie geht dann zurück an den, der sie verteilt hat.',
bleibt: 'Dein Satz bleibt an der Aufgabe stehen — damit niemand '
+ 'nachfragen muss.',
grund: 'Warum nicht?', grundPflicht: true,
platzhalter: 'z. B. bin diese Woche im Urlaub',
ja: 'Ablehnen', nein: 'Doch nicht',
});
if (!antwort || !antwort.grund) return;
var r = await schicken('/workspace/api/aufgaben/' + a.id + '/ablehnen',
{ grund: antwort.grund });
if (r.ok) { nachLaden(); return; }
sagen(meldung, window.sagWas ? window.sagWas(r.json.fehler) : 'Hat nicht geklappt.', false);
});
}
/* ==== EINE DAUERHAFTE AUFGABE WIRD NICHT FERTIG (30.09.2026) ==
VanVan im Support: „… dass die Aufgabe dauerhaft sein soll und
somit nicht vom Modi in den Status erledigt gesetzt werden
kann."
Sie ist keine Aufgabe, die man abarbeitet, sondern eine, die man
TUT. „Neue begrüßen" ist nicht fertig, wenn man es einmal
gemacht hat.
DIE MARKE STEHT FÜR ALLE DA, nicht nur für den Zugeteilten: Wer
die Karte ansieht, soll wissen, warum dort kein „Fertig" steht.
Ein fehlender Knopf ohne Erklärung liest sich wie ein Fehler. */
if (a.dauerhaft) {
var dauer = el('p', 'z-karte__zeile z-karte__zeile--dauer',
'Dauerhafte Aufgabe — sie wird nicht abgehakt, sie gehört dazu.');
kasten.append(dauer);
}
/* ==== HIER STANDEN „ICH FANGE AN" UND „FERTIG" (02.10.2026) ===
VanVan im Support: „Das dort steht ich bewerbe mich ist jetzt
weg, aber dafür hat er noch mal 2 Buttons hinzugefügt mit ich
fange an und fertig. Wenn man auf ich fange an drückt steht
dort in Bearbeitung und wenn man auf fertig drückt dann wird es
zu erledigt. Die Aufgabe bleibt aber im status offen stehen.
Die beiden Buttons können entfernt werden, weil es darüber ja
den button starten gibt, der auch korrekt funktioniert."
Sie hat recht, und sie hat etwas Groesseres gefunden als zwei
ueberfluessige Knoepfe: Es gab ZWEI Zustaende nebeneinander.
aufgaben.status offen · arbeit · review · erledigt
aufgaben_zuteilung.zustand angenommen · arbeit · erledigt
Diese Knoepfe setzten den zweiten (`/mein-stand`), der
Starten-Knopf darueber den ersten. Auf der Karte stand „in
Bearbeitung", in der Liste „offen" -- und beides stimmte. Das
ist schlimmer als ein Fehler, denn es gibt nichts, dem man
glauben kann.
ZWEI ANTWORTEN AUF DIESELBE FRAGE sind im Haus verboten, und
das ist genau der Fall gewesen. Es bleibt die eine, die VanVan
benennt: der Status der Aufgabe.
WAS ZUSAETZLICH NOETIG WAR, damit das Entfernen keine stille
Verschlechterung ist: Die Zaehler einer Person („offen / in
Arbeit / erledigt") lesen die ZUTEILUNG, nicht die Aufgabe.
Ohne Gegenmassnahme waere jede Zuteilung fuer immer auf
„angenommen" stehen geblieben. Deshalb zieht der Statuswechsel
die Zuteilung jetzt mit (`zuteilungenNachStatus` im Server).
NACHTRAG 03.10.2026 -- UND HIER STAND EINE AUSREDE. Woertlich:
„Der Weg `/mein-stand` bleibt bestehen -- er ist die Schranke,
falls ihn jemand direkt anspricht."
Eine Route ist keine Schranke gegen sich selbst. Sie setzte
weiterhin NUR den Zuteilungszustand und liess den
Aufgabenstatus stehen -- also genau den Widerspruch, den
VanVan gemeldet hat. Ich hatte ihn unsichtbar gemacht, nicht
abgeschafft: kein Knopf mehr, das Verhalten unveraendert im
System. Aufgefallen ist es beim Nachmessen am laufenden
Server, nicht beim Schreiben.
Die Route ist seither entfernt (siehe workspace-zuteilung.js).
Es gibt nur noch den Status -- eine Frage, eine Antwort. */
/* (2b) DIE BEWERBUNGEN — nur fuer die, die entscheiden duerfen.
JEDE BEWERBUNG BEKOMMT IHRE EIGENE ZEILE mit zwei Knoepfen.
Eine Auswahlliste mit einem Absenden-Knopf waere kuerzer und
falsch: Man entscheidet ueber einen Menschen, nicht ueber einen
Listeneintrag -- und wer drei Bewerbungen hat, will jede
einzeln beantworten koennen.
DER KOMMENTAR IST FREIWILLIG, bei beiden Knoepfen. Filipes Wort
ist „als moeglichkeit". */
if (darfEntscheiden && bewerbungen.length) {
var bBlock = el('div', 'z-bewerbungen');
bBlock.append(el('p', 'z-bewerbungen__schild',
bewerbungen.length === 1
? 'Eine Bewerbung wartet auf deine Antwort:'
: bewerbungen.length + ' Bewerbungen warten auf deine Antwort:'));
bewerbungen.forEach(function (z, i) {
var wem = z.person_name || 'Jemand';
var reihe = el('div', 'z-bewerbung');
reihe.append(el('span', 'z-bewerbung__name', wem));
/* DER ERSTE WIRD BENANNT, nicht nur oben hingestellt. Eine
Reihenfolge allein sieht man nicht an -- man haelt sie fuer
Zufall oder Alphabet. Erst das Wort macht daraus eine
Auskunft: Wer zuerst da war, steht als Erster da, und man
sieht, dass das kein Zufall ist.
Nur bei mehr als einer: Bei einer einzigen Bewerbung ist
„zuerst da" keine Auskunft, sondern Fuellwerk. */
if (i === 0 && bewerbungen.length > 1) {
reihe.append(el('span', 'z-bewerbung__zuerst', 'zuerst da'));
}
if (z.grund) reihe.append(el('span', 'z-bewerbung__wort', '„' + z.grund + '“'));
[['annehmen', 'Annehmen', 'knopf--klar'],
['ablehnen', 'Ablehnen', ''],
].forEach(function (w) {
var weg = w[0], schild = w[1], klasse = w[2];
var b = el('button', 'knopf knopf--klein ' + klasse, schild);
b.type = 'button';
b.addEventListener('click', async function () {
var antwort = await window.frageNach({
titel: schild + ' — ' + wem,
was: weg === 'annehmen'
? wem + ' bekommt die Aufgabe und wird benachrichtigt.'
: wem + ' bekommt sie nicht. Die Aufgabe bleibt frei.',
bleibt: 'Dein Kommentar bleibt an der Aufgabe stehen und '
+ wem + ' kann ihn lesen.',
grund: 'Willst du etwas dazuschreiben? (freiwillig)',
grundPflicht: false,
platzhalter: weg === 'annehmen'
? 'z. B. danke dir, mach das gern'
: 'z. B. das macht diesmal jemand anders',
ja: schild, nein: 'Abbrechen',
});
if (!antwort) return;
var r = await schicken(
'/workspace/api/aufgaben/' + a.id + '/bewerbung/' + weg,
{ person_id: z.person_id, kommentar: (antwort && antwort.grund) || '' });
if (r.ok) { nachLaden(); return; }
sagen(meldung, r.code === 409 && r.json.wer
? (r.json.wer + ' hat sie inzwischen — die Bewerbung ist hinfällig.')
: (window.sagWas ? window.sagWas(r.json.fehler) : 'Hat nicht geklappt.'), false);
});
reihe.append(b);
});
bBlock.append(reihe);
});
kasten.append(bBlock);
}
/* (3) BEWERTEN -- nur wer verteilen darf, und nur bei Menschen,
die wirklich fertig sind. */
if (ich && ich.darf_verteilen) {
/* DREI BENANNTE KNÖPFE STATT EINER AUSWAHL IM DIALOG.
Abschnitt 5 nennt drei Möglichkeiten. Sie als Auswahl in einen
Dialog zu legen hieße: erst klicken, dann lesen, dann wählen.
Als drei Knöpfe stehen sie da, man sieht sofort, was es gibt,
und ein Klick genügt. "Wenn Verbesserungsmöglichkeiten
vorhanden sind, soll ein Eingabebereich erscheinen" — genau
das passiert: Bei diesem einen Knopf ist der Text Pflicht, bei
den anderen beiden freiwillig. */
zu.filter(function (z) { return z.zustand === 'erledigt' && !z.bewertung; })
.forEach(function (z) {
var wem = z.person_name || 'sie';
var reihe = el('div', 'z-bewerten');
reihe.append(el('span', 'z-bewerten__schild', 'Rückmeldung an ' + wem + ':'));
[['gut', 'Gut gemacht', false],
['nicht_gut', 'Nicht gut gemacht', false],
['verbessern', 'Verbesserungsmöglichkeiten', true],
].forEach(function (w) {
var art = w[0], schild = w[1], pflicht = w[2];
var b = el('button', 'knopf z-bewerten__knopf', schild);
b.type = 'button';
b.dataset.art = art;
b.addEventListener('click', async function () {
var antwort = await window.frageNach({
titel: schild + ' — ' + wem,
was: pflicht
? 'Schreib konkret, was beim nächsten Mal besser gehen kann.'
: 'Du kannst noch einen Satz dazuschreiben — musst aber nicht.',
bleibt: 'Die Rückmeldung bleibt dauerhaft an dieser Aufgabe und '
+ wem + ' kann sie lesen.',
grund: pflicht ? 'Was genau?' : 'Noch etwas dazu? (freiwillig)',
grundPflicht: pflicht,
platzhalter: pflicht
? 'z. B. beim nächsten Mal bitte die Zeitstempel mitschreiben'
: 'z. B. sehr schnell gemacht, danke',
ja: 'Absenden', nein: 'Abbrechen',
});
if (!antwort) return;
var r = await schicken('/workspace/api/aufgaben/' + a.id + '/bewerten',
{ person_id: z.person_id, bewertung: art, text: antwort.grund || '' });
if (r.ok) { nachLaden(); return; }
sagen(meldung, r.json.fehler === 'text_fehlt'
? 'Bei „Verbesserungsmöglichkeiten“ gehört ein Satz dazu — sonst weiß '
+ wem + ' nicht, was gemeint ist.'
: (window.sagWas ? window.sagWas(r.json.fehler) : 'Hat nicht geklappt.'), false);
});
reihe.append(b);
});
kasten.append(reihe);
});
}
if (knoepfe.childElementCount) kasten.append(knoepfe);
/* NICHTS ZU ZEIGEN HEISST NICHTS ZEIGEN. Seit der Block auch bei
Aufgaben ohne Zuteilung gebaut wird, kann er leer bleiben -- ein
leerer Kasten unter jeder Karte waere Rauschen. */
if (!kasten.childElementCount) return null;
kasten.append(meldung);
return kasten;
}
/* =====================================================================
NACH AUSSEN
===================================================================== */
window.Zuteilung = {
meinBand: meinBand,
teamBand: teamBand,
karteBlock: karteBlock,
/* Holt beides und baut das passende Band. Der Aufrufer muss nicht
wissen, welche Rolle welches bekommt. */
bandBauen: async function (ich) {
try {
if (ich && ich.darf_verteilen) {
var a = await fetch('/workspace/api/aufgaben/team-resuemee');
if (!a.ok) return null;
return teamBand(await a.json());
}
var b = await fetch('/workspace/api/aufgaben/resuemee');
if (!b.ok) return null;
return meinBand(await b.json());
} catch (e) { return null; }
},
};
})();