/* ===================================================================== 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. Tippe eine an, um nur ihre 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 || []; if (!zu.length) return null; 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') { kasten.append(el('p', 'z-karte__zeile', 'Frei für ' + zu.length + ' Leute — wer zuerst Zeit hat')); } var wer = el('ul', 'z-wer'); zu.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)); } wer.append(zeile); }); 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; } if (mein && mein.zustand === 'offen') { if (a.verteilart === 'pool') { 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); }); } 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); }); } if (mein && (mein.zustand === 'angenommen' || mein.zustand === 'arbeit')) { if (mein.zustand === 'angenommen') { knopf('Ich fange an', '', async function (b) { b.disabled = true; var r = await schicken('/workspace/api/aufgaben/' + a.id + '/mein-stand', { zustand: 'arbeit' }); if (r.ok) { nachLaden(); return; } b.disabled = false; sagen(meldung, window.sagWas ? window.sagWas(r.json.fehler) : 'Hat nicht geklappt.', false); }); } knopf('Fertig', 'knopf--klar', async function (b) { b.disabled = true; var r = await schicken('/workspace/api/aufgaben/' + a.id + '/mein-stand', { zustand: 'erledigt' }); if (r.ok) { nachLaden(); return; } b.disabled = false; sagen(meldung, window.sagWas ? window.sagWas(r.json.fehler) : 'Hat nicht geklappt.', false); }); } /* (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); 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; } }, }; })();