Vorlagenbrett: Modis bewerben sich, DogFather und die rechte Hand entscheiden

Filipe: "die modis sollen bei all diesen voschlaegen auch nur bewerben
koennen. die aufgaben aus der vorlage, da sollen die modis sich nur
bewerben koennen und nur dogfather und die rechte hand sollen annehmen
oder ablehnen koennen, mit einem text als notiz."

WAS SICH AENDERT
----------------
Auf dem Vorlagenbrett steht fuer einen Modi jetzt "Bewerben" statt
"Uebernehmen". Wer sich beworben hat, sieht das an der Karte -- samt
dem Satz, WER antwortet, und einem Weg zurueck. DogFather und die
rechte Hand sehen die Bewerbung an derselben Karte, mit Namen und dem
Wort dazu, und daneben "Annehmen" und "Ablehnen". Beide fragen nach
einer Notiz.

"ALSO NUR" GILT AUCH AM SERVER, nicht nur im Browser: Die alte Tuer
antwortet einem Modi mit 403 und dem Satz, was stattdessen geht. Ein
ausgeblendeter Knopf ist eine Bitte, abgelehnt wird in der Route.

DIE LINKE HAND STEHT ABSICHTLICH NICHT BEI DEN ENTSCHEIDERN
------------------------------------------------------------
Sie gehoert seit dem 22.09. ueberall dazu ("ich will dass die linke
hand auch ueberall zu sehen ist"). Hier hat Filipe genau zwei genannt.
Das ist keine Vergesslichkeit von mir, sondern seine Aufzaehlung -- und
dieselbe Grenze zieht das Haus schon bei den Aufgaben-Bewerbungen
(entscheidetUeberAufgaben). Sie darf weiter VERTEILEN; das hat er nicht
angefasst.

Sie ist deshalb die schaerfste Probe in der Pruefung: Wer statt "darf
entscheiden" nur "darf verteilen" abfragt, laesst sie mitentscheiden --
und niemandem faellt es auf, weil alles funktioniert.

DIE AUFGABE ENTSTEHT ERST MIT DER ZUSAGE
----------------------------------------
Der naheliegende Weg waere gewesen, beim Bewerben gleich die Aufgabe
anzulegen und die vorhandene Bewerbung aus aufgaben_zuteilung
daranzuhaengen. Dann stuende nach zwoelf Absagen zwoelfmal Arbeit auf
dem Brett, die niemand bestellt hat -- und um das einzufangen, muesste
das Ablehnen Aufgaben LOESCHEN. Loeschen als Nebenwirkung einer Absage
ist genau die Sorte Regel, die irgendwann das Falsche trifft.

Also eine eigene, kleine Tabelle (vorlagen_bewerbungen). Bis jemand ja
sagt, gibt es nur eine Zeile. Die Woerter sind dieselben wie drueben
(zustand, entscheid_text, entschieden_von) -- zwei Namen fuer dieselbe
Sache waeren zwei Sprachen im selben Haus.

Und die Zusage legt die Aufgabe ueber DIESELBE Funktion an wie das
Uebernehmen (katalogAufgabeAnlegen, neu, aus dem Katalog-Zweig
herausgeloest). Damit sieht eine erbetene Aufgabe aus wie eine
verteilte: gleiche Frist, gleiche Kategorie, gleiche Kennung. Ein
zweiter Weg waere ein zweiter Satz Regeln.

KLEINIGKEITEN, DIE SONST WEHTUN
-------------------------------
  * "Alle 12 uebernehmen" gibt es nur fuer die, die verteilen. Ein
    "Alle bewerben" waere der schnellste Weg, zwoelf Bitten auf einmal
    loszuschicken -- und damit zwoelf Entscheidungen fuer jemand anderen.
  * Wer eine Aufgabe schon hat, bekommt keinen Bewerben-Knopf. Der
    Server lehnt das ohnehin ab; ein Knopf, der eine Absage holt, ist
    schlimmer als keiner.
  * Nach einer Absage darf man sich wieder bewerben. Der eindeutige
    Index gilt deshalb nur fuer OFFENE Bewerbungen -- ueber alle
    Zustaende waere eine Absage ein Bann.
  * Gesucht wird ueber den SCHLUESSEL der Vorlage, nicht ueber die
    Nummer in der Liste. Die Nummer verschiebt sich, sobald jemand eine
    Vorlage einfuegt -- genau dieser Fehler ist am 16.09. schon einmal
    passiert.

GEPRUEFT
--------
pruef-modi-katalog: 95 Pruefungen, 0 Fehler (vorher 49).

Die Pruefung ist beim Umbau ROT geworden -- 9 Zeilen, alle dort, wo ein
Modi sich selbst etwas nahm. Richtig so, sie hat die Aenderung bemerkt.
Sie steht jetzt auf dem neuen Weg und misst ihn ganz:

  * der Modi kommt an die alte Tuer nicht mehr heran (403, erst_bewerben)
  * die Bewerbung legt NOCH KEINE Aufgabe an
  * die linke Hand darf verteilen, aber nicht entscheiden (403)
  * der Bewerber selbst erst recht nicht (403)
  * die Zusage erzeugt die Aufgabe -- mit Kategorie, Frist, Besitzer
  * die Notizen stehen in der Datenbank, samt WER entschieden hat
    (direkt gelesen: ein Feld, das der Server annimmt und nirgends
    speichert, saehe von aussen genauso aus)
  * nach einer Absage geht es wieder
  * am Bildschirm: alle Knoepfe heissen "Bewerben", kein einziger
    "Uebernehmen" mehr, die wartende Karte nennt, wer antwortet --
    und DogFather klickt sich durch Annehmen samt Notizfeld, bis die
    Aufgabe auf dem Brett steht

Die Gegenprobe in Abschnitt 7 lief mit dem Zugang des Modis und haette
ab heute nur noch bewiesen, dass die Rechtepruefung greift -- sie
benutzt jetzt DogFather. Genau so verliert eine Pruefung still ihren
Sinn.

pruef-aufgaben-vorlagen: unveraendert gruen.

ZWEI FUNDE NEBENHER, BEIDE AELTER ALS DIESE AENDERUNG -- gemessen, nicht
vermutet (mit gestashten Aenderungen gegengeprueft):
  * pruef-modi-wortleck ist seit dem 22.09. rot: Der Rollenname steht
    in team.css und teamlage.js, also in Dateien, die jeder bekommt.
  * pruef-zuteilung stuerzt seit laengerem ab (#neu-oeffnen ist
    verborgen). Beides kommt als naechstes, getrennt.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-23 10:59:51 +02:00
co-authored by Claude Opus 5
parent 256ea6f7c0
commit e736a12cff
45 changed files with 1652 additions and 649 deletions
+250 -20
View File
@@ -1443,6 +1443,18 @@
ist. Ohne dieses Merkmal fragte die Seite bei jedem Neuzeichnen
erneut nach -- und bekaeme jedes Mal wieder nichts. */
let vorlagenGeladen = false;
/* ==== BEWERBUNGEN AUF EINE VORLAGE (23.09.2026) ====================
Filipe: „die modis sollen bei all diesen voschlägen auch nur
bewerben können ... und nur dogfather und die rechte hand sollen
annehmen oder ablehnen können, mit einem text als notiz."
WER WAS DARF, SAGT DER SERVER. Hier steht kein Rollenname -- diese
Datei bekommt jeder, der die Seite öffnet, und genau daran ist
dieses Brett am 17.09. schon einmal gescheitert. */
let bewerbungen = [];
let darfEntscheidenV = false;
let darfVerteilenV = false;
let vBereich = 'live';
let vStufe = 'anfaenger';
@@ -1465,6 +1477,36 @@
let vOffen = false;
try { vOffen = localStorage.getItem(VORLAGEN_OFFEN) === 'ja'; } catch { vOffen = false; }
/** Den Katalog holen -- und mit ihm die Bewerbungen.
*
* ALS EIGENE FUNKTION, weil sie zweimal gebraucht wird: beim ersten
* Aufbau und nach JEDER Entscheidung. Ohne das zweite bliebe eine
* beantwortete Bewerbung stehen, bis jemand die Seite neu lädt --
* und zwei Leute würden über dieselbe Bitte zweimal entscheiden.
*
* DIE BEWERBUNGEN KOMMEN IN DERSELBEN ANTWORT wie der Katalog. Eine
* zweite Abfrage daneben wäre ein zweiter Weg, auf dem die beiden
* auseinanderlaufen können. */
async function vorlagenHolen() {
try {
const a = await hole('/workspace/api/vorlagen');
if (!a.ok) return false;
const d = await a.json();
vorlagen = d.aufgaben || null;
/* Ein Katalog mit null Eintraegen ist keiner. Ohne diese Pruefung
haette ein leeres `{gruppen:[],aufgaben:[]}` einen Block mit
Ueberschrift und nichts darin ergeben -- das sieht aus wie ein
Fehler, und es waere auch einer. */
katalog = d.katalog && Array.isArray(d.katalog.aufgaben) && d.katalog.aufgaben.length
? d.katalog : null;
bewerbungen = Array.isArray(d.bewerbungen) ? d.bewerbungen : [];
darfEntscheidenV = !!d.darf_entscheiden;
darfVerteilenV = !!d.darf_verteilen;
vorlagenGeladen = true;
return true;
} catch { return false; }
}
async function vorlagenbrettZeigen() {
const ziel = $('vorlagenbrett');
if (!ziel || !ich) return;
@@ -1477,19 +1519,7 @@
verborgen. Dieselbe Wirkung, ohne eine Zeile, die verraet, fuer
wen sie gilt. */
if (!vorlagenGeladen) {
try {
const a = await hole('/workspace/api/vorlagen');
if (!a.ok) return;
const d = await a.json();
vorlagen = d.aufgaben || null;
/* Ein Katalog mit null Eintraegen ist keiner. Ohne diese Pruefung
haette ein leeres `{gruppen:[],aufgaben:[]}` einen Block mit
Ueberschrift und nichts darin ergeben -- das sieht aus wie ein
Fehler, und es waere auch einer. */
katalog = d.katalog && Array.isArray(d.katalog.aufgaben) && d.katalog.aufgaben.length
? d.katalog : null;
vorlagenGeladen = true;
} catch { return; }
if (!(await vorlagenHolen())) return;
}
/* Zwei Kataloge, einer je Person -- nie beide. Wer den einen
bekommt, bekommt den anderen nicht; das entscheidet der Server. */
@@ -1880,13 +1910,91 @@
fuss.append(el('span', 'vorlagenbrett__schon',
`schon übernommen · ${STATUS_NAME[schon.status] || schon.status}`));
}
const k = el('button', 'knopf knopf--klein', schon ? 'Nochmal' : 'Übernehmen');
k.type = 'button';
if (schon) k.title = 'Diese Vorlage liegt schon auf dem Brett – noch einmal übernehmen?';
k.addEventListener('click', () => uebernehmen(k,
{ art: 'katalog', nr: v.nr, gruppe: kGruppe, person_id: katalogZiel() }));
fuss.append(k);
/* ==== ÜBERNEHMEN ODER BEWERBEN? (23.09.2026) ==================
Filipe: „die modis sollen bei all diesen voschlägen auch nur
bewerben können."
DER SERVER SAGT, WAS ICH DARF (`darf_verteilen`). Hier steht
kein Rollenname — diese Datei bekommt jeder, der die Seite
öffnet. Und die Schranke selbst liegt ohnehin in der Route:
Ein ausgeblendeter Knopf ist eine Bitte, abgelehnt wird am
Server. */
const meine = bewerbungen.find((b2) => b2.selbst && b2.vorlage === v.schluessel);
if (darfVerteilenV) {
const k = el('button', 'knopf knopf--klein', schon ? 'Nochmal' : 'Übernehmen');
k.type = 'button';
if (schon) k.title = 'Diese Vorlage liegt schon auf dem Brett – noch einmal übernehmen?';
k.addEventListener('click', () => uebernehmen(k,
{ art: 'katalog', nr: v.nr, gruppe: kGruppe, person_id: katalogZiel() }));
fuss.append(k);
} else if (meine) {
/* SCHON BEWORBEN. Der Satz sagt, WER antwortet -- sonst wartet
man auf niemanden und fragt am nächsten Tag im Chat. */
fuss.append(el('span', 'vorlagenbrett__warte',
'beworben – DogFather oder die rechte Hand antworten'));
const zurueck = el('button', 'knopf knopf--klein', 'Zurückziehen');
zurueck.type = 'button';
zurueck.addEventListener('click', () => bewerbungZurueck(zurueck, meine));
fuss.append(zurueck);
} else if (!schon) {
const k = el('button', 'knopf knopf--klein', 'Bewerben');
k.type = 'button';
k.title = 'DogFather oder die rechte Hand entscheiden, ob du sie bekommst.';
k.addEventListener('click', () => bewerben(k, v));
fuss.append(k);
}
/* WER SIE SCHON HAT, BEKOMMT KEINEN BEWERBEN-KNOPF. Der Server
lehnt das ohnehin ab (liegt_schon_bei_dir) -- ein Knopf, der
eine Absage holt, ist schlimmer als keiner. Der Vermerk „schon
übernommen" steht weiter daneben; er ist die Antwort auf die
Frage, warum hier nichts zu tun ist. */
karte.append(fuss);
/* ==== DIE BEWERBUNGEN ANDERER -- nur für die, die entscheiden ==
Sie stehen AN DER KARTE und nicht in einer eigenen Liste
irgendwo: Entschieden wird über eine bestimmte Aufgabe, und
die Frage „will ich, dass Frida das macht?" beantwortet man
besser mit dem Text der Aufgabe davor.
JE MENSCH EINE ZEILE, nicht ein Knopf für alle. Man entscheidet
über einen Menschen, nicht über einen Listeneintrag — dieselbe
Überlegung wie am Aufgabenbrett. */
if (darfEntscheidenV) {
const offeneB = bewerbungen.filter((b2) => b2.vorlage === v.schluessel);
if (offeneB.length) {
karte.dataset.beworben = 'ja';
const block = el('div', 'vorlagenbrett__bewerbungen');
block.append(el('p', 'vorlagenbrett__bschild', offeneB.length === 1
? 'Eine Bewerbung wartet auf deine Antwort:'
: `${offeneB.length} Bewerbungen warten auf deine Antwort:`));
offeneB.forEach((b2, i) => {
const reihe = el('div', 'vorlagenbrett__bewerbung');
reihe.append(el('span', 'vorlagenbrett__bname', b2.person_name));
/* DER ERSTE WIRD BENANNT, nicht nur oben hingestellt. Eine
Reihenfolge allein hält man für Zufall oder Alphabet.
Nur bei mehr als einer -- sonst ist es Füllwerk. */
if (i === 0 && offeneB.length > 1) {
reihe.append(el('span', 'vorlagenbrett__bzuerst', 'zuerst da'));
}
if (b2.text) reihe.append(el('span', 'vorlagenbrett__bwort', `„${b2.text}"`));
for (const [weg, schild, klasse] of [
['annehmen', 'Annehmen', 'knopf--klar'],
['ablehnen', 'Ablehnen', ''],
]) {
const b3 = el('button', `knopf knopf--klein ${klasse}`, schild);
b3.type = 'button';
b3.addEventListener('click', () => entscheiden(b3, weg, schild, b2, v));
reihe.append(b3);
}
block.append(reihe);
});
karte.append(block);
}
}
box.append(karte);
}
@@ -1898,7 +2006,13 @@
}
koerper.append(box);
if (liste.length > 1) {
/* „ALLE ÜBERNEHMEN" GIBT ES NUR FÜR DIE, DIE VERTEILEN (23.09.2026).
Ein „Alle bewerben" waere der schnellste Weg, zwölf Bitten auf
einmal loszuschicken -- und damit zwölf Entscheidungen fuer
jemand anderen. Wer sich bewirbt, waehlt aus; das ist der
Unterschied zwischen einer Bewerbung und einem Griff ins Regal. */
if (liste.length > 1 && darfVerteilenV) {
const alle = el('button', 'abmelden', `Alle ${liste.length} übernehmen`);
alle.title = 'Legt alle Aufgaben an, die gerade in der Liste stehen – '
+ 'auch die, die schon auf dem Brett liegen.';
@@ -1929,6 +2043,122 @@
/* Ein Weg fuer beide Knoepfe -- einzeln und alle. Zwei fast gleiche
Funktionen waeren der sichere Weg dazu, dass die eine irgendwann
anders reagiert als die andere. */
/* =====================================================================
BEWERBEN, ZURÜCKZIEHEN, ENTSCHEIDEN (23.09.2026)
=====================================================================
DREI HANDGRIFFE, EIN MUSTER: fragen, schicken, frisch holen,
neu zeichnen. Das „frisch holen" ist der Teil, den man vergisst --
ohne ihn bliebe eine beantwortete Bewerbung stehen, bis jemand die
Seite neu lädt, und zwei Leute entschieden über dieselbe Bitte.
GEFRAGT WIRD MIT `frageNach`, dem Kasten des Hauses. Er bringt das
Textfeld für die Notiz gleich mit -- ein eigener wäre ein zweiter
Dialog mit eigener Tastaturbedienung. */
/** Nach einer Entscheidung: Katalog UND Brett neu -- in dieser
* Reihenfolge. Die Aufgabe, die gerade entstanden ist, soll man
* dort sehen, wo sie ab jetzt lebt. */
async function nachBewerbung() {
await vorlagenHolen();
await laden();
await vorlagenbrettZeigen();
}
async function bewerben(knopf, v) {
const antwort = await window.frageNach({
titel: 'Für diese Aufgabe bewerben?',
was: `„${v.titel}" – 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;
knopf.disabled = true;
try {
const a = await hole('/workspace/api/vorlagen/bewerben', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ vorlage: v.schluessel, text: (antwort && antwort.grund) || '' }),
});
if (!a.ok) {
const f = await a.json().catch(() => ({}));
melde(sagWas(f.fehler, 'Das hat nicht geklappt.'));
knopf.disabled = false;
return;
}
melde('');
await nachBewerbung();
} catch {
melde('Keine Verbindung – die Bewerbung ist nicht angekommen.');
knopf.disabled = false;
}
}
async function bewerbungZurueck(knopf, b) {
knopf.disabled = true;
try {
const a = await hole('/workspace/api/vorlagen/bewerbung-zurueck', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: b.id }),
});
if (!a.ok) {
const f = await a.json().catch(() => ({}));
melde(sagWas(f.fehler, 'Das hat nicht geklappt.'));
knopf.disabled = false;
return;
}
melde('');
await nachBewerbung();
} catch {
melde('Keine Verbindung.');
knopf.disabled = false;
}
}
async function entscheiden(knopf, weg, schild, b, v) {
const wem = b.person_name;
const antwort = await window.frageNach({
titel: `${schild} — ${wem}`,
was: weg === 'annehmen'
? `${wem} bekommt „${v.titel}" als Aufgabe auf das Brett.`
: `${wem} bekommt sie nicht. Die Vorlage bleibt für alle offen.`,
/* DER TEXT IST DER GRUND, WARUM FILIPE DAS SO WOLLTE: „mit einem
text als notiz". Eine Absage ohne Satz ist die, nach der
niemand weiß, woran es lag. */
bleibt: `Dein Text bleibt an der Entscheidung stehen.`,
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;
knopf.disabled = true;
try {
const a = await hole(`/workspace/api/vorlagen/bewerbung/${weg}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: b.id, kommentar: (antwort && antwort.grund) || '' }),
});
if (!a.ok) {
const f = await a.json().catch(() => ({}));
melde(sagWas(f.fehler, 'Das hat nicht geklappt.'));
knopf.disabled = false;
return;
}
melde('');
await nachBewerbung();
} catch {
melde('Keine Verbindung – die Entscheidung ist nicht angekommen.');
knopf.disabled = false;
}
}
async function uebernehmen(knopf, was, fuer) {
knopf.disabled = true;
const vorher = knopf.textContent;
+16
View File
@@ -265,6 +265,22 @@
'Dich selbst kannst du hier nicht eintragen. Bewirb dich dafür, dann '
+ 'entscheiden DogFather oder die rechte Hand.',
/* ---- Bewerbung auf eine Vorlage (23.09.2026) ----------------
Filipe: „die modis sollen bei all diesen voschlägen auch nur
bewerben können."
Beide Sätze sagen, was JETZT zu tun ist. „Geht nicht" allein
lässt jemanden vor einem Knopf stehen, der nichts tut. */
liegt_schon_bei_dir:
'Diese Aufgabe liegt schon auf deinem Brett – du musst dich nicht '
+ 'noch einmal dafür bewerben.',
vorlage_weg:
'Diese Vorlage gibt es nicht mehr. Die Bewerbung läuft ins Leere – '
+ 'lade die Seite neu, dann siehst du, was jetzt zur Wahl steht.',
person_weg:
'Dieser Zugang ist inzwischen abgeschaltet. Eine Aufgabe dorthin '
+ 'würde niemand mehr sehen.',
/* ---- Zeitraum und vergebene Stücke (22.09.2026) ------------- */
schon_im_umlauf:
'Das hat sich schon jemand genommen und postet es vielleicht '