Bewerbungen der Modis bekommen eine eigene Spalte im Aufgabenbrett

Filipe: "ich will in dieser seite auch eine eigene kategorie fuer die
aufgaben wo die modis sich selbst bewerben. ich will dass man die
getrennt sieht. mach es richtig uebersichtlich."

WO SIE VORHER STANDEN, und warum das nicht reichte: nur auf dem
Vorlagenbrett, an der jeweiligen Karte. Das ist der richtige Ort zum
ENTSCHEIDEN -- man liest Aufgabe und Name nebeneinander. Es ist der
falsche Ort zum SEHEN: Wer morgens das Aufgabenbrett oeffnet, sieht
nicht, dass drei Leute auf eine Antwort warten, und das Vorlagenbrett
ist eine Seite weiter. Fuer den Modi gab es ueberhaupt keine Stelle,
an der stand "dein Wunsch ist angekommen".

DIE SPALTE STEHT GANZ VORN. Sie ist die einzige, in der jemand auf eine
ANTWORT wartet -- alle anderen zeigen Arbeit, die laeuft. Und sie
VERSCHWINDET, wenn nichts wartet: Eine leere Spalte "Bewerbungen"
stuende 360 Tage im Jahr im Weg, um an fuenf Tagen etwas zu sagen. Die
vier festen Spalten haben auch leer eine Aussage ("hier landet, was
fertig ist"); diese nicht.

WAS AUF DER KARTE STEHT: der TITEL der Vorlage (nicht ihr Schluessel),
wer sie uebernehmen moechte, und sein eigener Satz dazu -- als Zitat
gesetzt, mit Strich davor. Er gehoert ihm, nicht dem Brett. Ohne ihn
entscheidet man ueber einen Namen.

DER TITEL WIRD NACHGESCHLAGEN, NICHT MITGESPEICHERT. Er gehoert dem
Katalog; stuende er in der Bewerbungszeile, gaebe es zwei Wahrheiten,
und die aeltere gewinnt still, sobald jemand eine Vorlage umbenennt.

EIN EIGENER, KLEINER WEG statt eines Mitschleppens:
`/workspace/api/vorlagen/bewerbungen` liefert nur die Bewerbungen --
nicht den ganzen Katalog, der an `/workspace/api/vorlagen` haengt. Das
Brett zeichnet sich bei jedem Statuswechsel neu; der Katalog ist um ein
Vielfaches groesser als die Handvoll Bewerbungen.

WER ENTSCHEIDEN DARF, SAGT DER SERVER (`darf_entscheiden`) -- nicht der
Rollenname im Browser. `assets/js` bekommt jeder, der die Seite
oeffnet, und ein Rollenvergleich dort ist in diesem Haus allein diese
Woche dreimal veraltet. Bis die Antwort da ist, gilt `false`: lieber
einen Knopf zu spaet zeigen als einen, der eine Absage holt.

DIE FARBE IST NEU IM BRETT. Die vier vorhandenen stehen fuer einen
Arbeitsstand (grau, blau, gelb, gruen); hier wartet niemand auf Arbeit,
sondern auf eine Entscheidung. Kein Rot ("kaputt"), kein Gelb (heisst
schon "zur Freigabe") -- Violett, das im Chat seit gestern fuer "an
dich gerichtet" steht. Eine Sprache im Haus.

Geprueft: pruef-bewerbung-aufgaben 101 -> 111 Punkte. Darunter die drei
Gegenproben, ohne die "die Spalte ist da" nichts bewiese: ohne
Bewerbung gibt es sie NICHT, der Modi bekommt KEINE
Entscheidungsknoepfe (sondern "wartet auf Antwort"), und die Spalte
steht wirklich an erster Stelle. Dazu pruef-aufgabenbrett und
pruef-vorlagen (24), beide unveraendert gruen.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-25 03:29:33 +02:00
co-authored by Claude Opus 5
parent 1a14eb7449
commit 8bf2f440d0
45 changed files with 1034 additions and 636 deletions
+172
View File
@@ -788,6 +788,39 @@
: alleAufgaben);
}
/* ==== DIE BEWERBUNGEN DER MODIS (25.09.2026) ====================
Filipe: „ich will in dieser seite auch eine eigene kategorie fuer
die aufgaben wo die modis sich selbst bewerben. ich will dass man
die getrennt sieht. mach es richtig uebersichtlich."
SIE STANDEN BISHER NUR AUF DEM VORLAGENBRETT, an der jeweiligen
Karte. Das ist der richtige Ort zum ENTSCHEIDEN -- man liest die
Aufgabe und den Namen nebeneinander. Es ist aber der falsche Ort
zum SEHEN: Wer morgens das Aufgabenbrett oeffnet, sieht nicht,
dass drei Leute auf eine Antwort warten, und das Vorlagenbrett
steht eine Seite weiter.
DER STAND WIRD GEMERKT, nicht bei jedem Zeichnen neu geholt: Das
Brett zeichnet sich bei jedem Statuswechsel neu, und ein Abruf je
Klick waere Last ohne Gegenwert. Geholt wird beim Laden und nach
jeder Entscheidung -- also genau dann, wenn sich etwas aendert. */
let bewerbungen = [];
/* OB ICH ENTSCHEIDEN DARF, SAGT DER SERVER -- nicht der Rollenname.
Bis die Antwort da ist, false: Lieber einen Knopf zu spaet zeigen
als einen, der eine Absage holt. */
let darfEntscheiden = false;
async function bewerbungenHolen() {
try {
const a = await hole('/workspace/api/vorlagen/bewerbungen');
if (!a.ok) return;
const d = await a.json();
bewerbungen = d.bewerbungen || [];
darfEntscheiden = d.darf_entscheiden === true;
} catch { /* hole() hat schon gemeldet */ }
}
function zeichne(aufgaben) {
const brett = $('brett');
brett.textContent = '';
@@ -846,6 +879,40 @@
brett.parentElement.insertBefore(band, brett);
}
/* ==== DIE SPALTE STEHT GANZ VORN ==============================
Sie ist die einzige, in der jemand auf eine ANTWORT wartet --
alle anderen zeigen Arbeit, die laeuft. Was auf eine Antwort
wartet, gehoert an die Stelle, die man zuerst ansieht.
SIE VERSCHWINDET, WENN NICHTS WARTET. Eine leere Spalte
„Bewerbungen" stuende 360 Tage im Jahr im Weg, um an fuenf
Tagen etwas zu sagen -- anders als die vier festen Spalten, die
auch leer eine Aussage haben („hier landet, was fertig ist").
DER MODI SIEHT SEINE EIGENE und nur die: Das entscheidet der
Server (`vorlagenBewerbungenFuer`), nicht diese Datei. Fuer ihn
ist sie die Antwort auf „ist mein Wunsch angekommen?" -- vorher
gab es darauf nirgends eine. */
if (bewerbungen.length) {
const spalte = el('section', 'spalte spalte--bewerbung');
spalte.dataset.status = 'beworben';
spalte.dataset.leer = 'nein';
spalte.style.setProperty('--wuchs', String(Math.min(bewerbungen.length, 6)));
const kopf = el('h2', 'spalte__kopf');
kopf.append(el('span', 'spalte__punkt'), el('span', null, 'Bewerbungen'),
el('span', 'spalte__zahl', String(bewerbungen.length)));
spalte.append(kopf);
spalte.append(el('p', 'spalte__unter', darfEntscheiden
? 'Modis möchten das übernehmen – du entscheidest'
: 'Du hast dich beworben – DogFather oder die rechte Hand antworten'));
const kasten = el('div', 'spalte__karten');
for (const b of bewerbungen) kasten.append(bewerbungsKarte(b));
spalte.append(kasten);
brett.append(spalte);
}
for (const s of SPALTEN) {
const eigene = gruppen.filter((g) => g.status === s.status);
/* ZWEI ZAHLEN, UND SIE SAGEN VERSCHIEDENES: wie viele STUECKE
@@ -923,6 +990,106 @@
}
}
/** Eine Karte fuer eine Bewerbung -- wer, worauf, und was er dazu
* geschrieben hat.
*
* SIE SIEHT AUS WIE DIE ANDEREN KARTEN und ist doch eine andere
* Sache: Es gibt sie noch nicht als Aufgabe. Deshalb dieselbe Form
* (man soll das Brett lesen koennen, ohne umzulernen) und ein
* eigenes Merkmal `data-art="bewerbung"` fuer die Farbe.
*
* DIE ZWEI KNOEPFE STEHEN NUR BEI DENEN, DIE ENTSCHEIDEN. Der Modi
* sieht stattdessen einen Satz: Sein Wunsch ist angekommen. Ein
* Knopf, der eine Absage holt, ist schlimmer als keiner.
*/
function bewerbungsKarte(b) {
const k = el('article', 'karte karte--bewerbung');
k.dataset.art = 'bewerbung';
const kopf = el('div', 'karte__kopf');
kopf.append(el('h3', 'karte__titel', b.titel));
k.append(kopf);
/* WER ES SEIN MOECHTE steht gross, nicht klein: Ueber die Person
wird hier entschieden, nicht ueber den Titel. Den kennt man
vom Vorlagenbrett. */
const wer = el('p', 'karte__wer');
wer.append(el('strong', null, b.selbst ? 'Du' : b.person_name));
wer.append(document.createTextNode(b.selbst
? ' hast dich beworben' : ' möchte das übernehmen'));
k.append(wer);
if (b.text) {
/* SEIN SATZ DAZU, als Zitat gesetzt -- er gehoert ihm, nicht
dem Brett. Ohne ihn entscheidet man ueber einen Namen. */
const zitat = el('p', 'karte__bwort');
zitat.textContent = b.text;
k.append(zitat);
}
const fuss = el('div', 'karte__fuss');
if (darfEntscheiden) {
const ja = el('button', 'knopf knopf--klein', 'Annehmen');
ja.type = 'button';
ja.addEventListener('click', () => bewerbungEntscheiden(ja, b, true));
const nein = el('button', 'knopf knopf--klein karte__ablehnen', 'Ablehnen');
nein.type = 'button';
nein.addEventListener('click', () => bewerbungEntscheiden(nein, b, false));
fuss.append(ja, nein);
} else {
fuss.append(el('span', 'karte__warte', 'wartet auf Antwort'));
}
k.append(fuss);
return k;
}
/** Annehmen oder ablehnen -- mit der Moeglichkeit, ein Wort
* dazuzuschreiben.
*
* DIE NOTIZ IST FREIWILLIG, aber sie wird GEFRAGT: Eine Absage ohne
* Grund ist die Sorte Antwort, nach der man im Chat nachfragt --
* und dann steht dieselbe Erklaerung doppelt. Abbrechen des
* Dialogs bricht die ganze Handlung ab; sonst haette man aus
* Versehen zugesagt, weil man das Fenster wegklicken wollte.
*/
async function bewerbungEntscheiden(knopf, b, ja) {
const wer = b.selbst ? 'dich' : b.person_name;
const antwort = await window.frageNach({
titel: ja ? `„${b.titel}" an ${b.person_name}?` : `${b.person_name} absagen?`,
was: ja
? `Die Aufgabe wird angelegt und liegt danach bei ${b.person_name}.`
: `Die Vorlage bleibt auf dem Brett – ${wer} kann sich später wieder bewerben.`,
grund: 'Willst du etwas dazuschreiben? (freiwillig)',
grundPflicht: false,
platzhalter: ja ? 'z. B. super, mach das gern' : 'z. B. diesmal nicht, nächste Woche gern',
ja: ja ? 'Annehmen' : 'Ablehnen',
nein: 'Doch nicht',
});
/* `frageNach` gibt bei „Doch nicht" etwas Falsches zurueck --
dann ist NICHTS passiert. Das ist wichtiger als es klingt: Wer
das Fenster wegklickt, weil er sich vertan hat, darf damit
keine Zusage ausloesen. */
if (!antwort) return;
const wort = (antwort && antwort.grund) || '';
knopf.disabled = true;
try {
const a = await hole(`/workspace/api/vorlagen/bewerbung/${ja ? 'annehmen' : 'ablehnen'}`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: b.id, kommentar: wort || undefined }),
});
if (!a.ok) { melde('Das ließ sich gerade nicht entscheiden.'); return; }
const antwort = await a.json().catch(() => ({}));
/* DIE LISTE KOMMT AUS DER ANTWORT. Sie im Browser
zusammenzustreichen waere eine zweite Wahrheit -- und bei
zwei offenen Fenstern die falsche. */
if (Array.isArray(antwort.bewerbungen)) bewerbungen = antwort.bewerbungen;
else await bewerbungenHolen();
await laden();
} catch { /* hole() hat schon gemeldet */ } finally {
knopf.disabled = false;
}
}
/** Eine Karte fuer ein Stueck Arbeit, an dem mehrere sitzen. */
function sammelKarte(g) {
const a = g.erste;
@@ -1420,6 +1587,11 @@
kanalFeldZeigen('f');
kanalFeldZeigen('b');
lageZeigen(alleAufgaben);
/* ERST DIE BEWERBUNGEN, DANN ZEICHNEN. Andersherum entstuende
das Brett ohne die Spalte und bekaeme sie erst beim naechsten
Mal -- ein Flackern, bei dem drei wartende Menschen eine
Sekunde lang nicht dastehen. */
await bewerbungenHolen();
neuZeichnen();
bandZeigen();
/* Erst NACH dem Zeichnen -- vorher gibt es die Karten nicht, und