Files
dogfather-universe/workspace/assets/js/teamlage.js
T
DogFatherGitandClaude Opus 5 65d278a18e Der Handy-Rundgang ist zum ersten Mal bei null Befunden
SECHS SACHEN, UND KEINE DAVON WAR DAS, WONACH ICH GESUCHT HABE.

1. ZWEI NATIVE prompt() IN teamlage.js -- die letzten im Haus.
   Sie halten die ganze Seite an, sehen auf jedem Browser anders aus
   als der Rest und koennen nicht sagen, was BLEIBT, wenn man absagt.
   Genau das ist dort die Frage, die jemand vor dem Klicken hat. Jetzt
   derselbe Dialog wie an den 42 anderen Stellen.

2. UND DER GRUND, WARUM SIE NIEMAND GEFUNDEN HAT.
   pruef-nachfrage suchte mit `(?:^|[^.\w])(confirm|prompt)\s*\(`.
   Das `[^.\w]` sollte fremde Methoden ausschliessen -- `angebot
   .prompt()` ist die Installations-Aufforderung des Browsers und kein
   nativer Dialog. Nur trifft dieser Ausschluss ausgerechnet die
   HAEUFIGSTE Schreibweise: `window.prompt(` hat einen Punkt davor.
   Die Pruefung war gruen, waehrend zwei native prompt() dastanden.
   Genau das Muster, vor dem dieses Haus warnt: eine Pruefung, die
   laeuft, gruen ist und das Falsche prueft. Die Gegenprobe kennt
   jetzt beide Schreibweisen -- haette sie das vorher getan, waere es
   am selben Tag aufgefallen.

3. willkommen.html LUD nachfrage.js NICHT.
   Gefunden von der geschaerften Pruefung. kopf.js ruft `frageNach(`
   ohne Absicherung -- auf dieser einen Seite haette ABMELDEN einen
   Absturz ausgeloest. Die Seite ist vom 21.09., die Luecke also
   einen Tag alt.

4. ZWEI FEHLALARME IM HANDY-RUNDGANG ABGESTELLT.
   Er meldete bei JEDEM Lauf dieselben drei Befunde: `button.schnitt
   bis 481px`, die Rechtetafel `table bis 877px`, `a.k-pille 8x8`.
   Nachgemessen bei 390 px: Das Dokument ist exakt 390 px breit,
   NICHTS laeuft ueber -- beide stehen in einem Kasten mit
   `overflow-x: auto`, und der rollt absichtlich. Die Kalenderpunkte
   tragen `pointer-events: none`; der Tipp gehoert der Tageszelle.
   Eine Warnung, die immer kommt, ist keine Warnung mehr (Hausregel
   vom 03.09.). Beide Regeln sind SCHMAL: nur ausdrueckliches
   `overflow-x: auto|scroll` (nicht `hidden` -- dort ist der Inhalt
   wirklich weg), nur ausdrueckliches `pointer-events: none`.
   Die Gegenprobe hat jetzt vier Faelle statt zwei: zwei, die gemeldet
   werden MUESSEN, und zwei, die es NICHT duerfen. Eine engere Messung
   kann auch zu eng sein.

5. DER LETZTE ECHTE BEFUND: 404 BEI JEDEM MODI.
   Der Rundgang meldete "404 (Not Found)" ohne Adresse -- eine
   Pruefung, die einen Fehler findet, ihn aber nicht auffindbar macht,
   kostet mehr Zeit als sie spart. Sie nennt jetzt die Adresse, und
   damit war es in einer Minute klar: `/workspace/api/werdegang/liste`.
   Der Code BEHANDELTE den 404 richtig, der Browser protokolliert ihn
   trotzdem -- derselbe Fall wie am 19.09. bei /anruf/adressen.
   Jetzt sagt der Server in /api/ich, ob jemand Team Dogi fuehrt.
   EIGENES FELD, kein Stellvertreter: `darf_verteilen` sieht aehnlich
   aus, ist aber nicht dasselbe -- ein Manager darf verteilen und
   fuehrt Team Dogi nicht.

   Dabei EINE Wartestelle statt zwei: `window.wennIchDaBin()`.
   `window.__ich` kommt ueber das Netz; zwei Seiten hatten dafuer
   jeweils ein eigenes setInterval. Zwei Fassungen desselben Wartens
   altern unterschiedlich.

6. pruef-werdegang MASS AUF DER FALSCHEN ADRESSE.
   Drei Pruefungen waren dauerhaft rot (`data-ton=null`) -- an einer
   Seite, die in Ordnung ist. Sie oeffnete `127.0.0.1`, also die
   Adresse der AGENTUR; dort ist `person.haus` nicht "crew" und die
   Kachelliste eine andere. Nachgemessen: Alle vier Rollen HABEN die
   Kachel, sobald das Haus stimmt. Jetzt derselbe https-Vorbau wie in
   pruef-willkommen und pruef-zuteilung.

   Beinahe haette ich hier etwas "repariert", das nicht kaputt war:
   Meine erste Messung rief `bereicheFuer(p, "crew")` auf -- das Haus
   wird aber aus `person.haus` gelesen, nicht als Argument. Sie sagte
   "DogFather hat keine Kachel". Eine plausible Herleitung ersetzt
   keine Messung, und eine falsch aufgesetzte Messung auch nicht.

GEMESSEN: pruef-handy-teamdogi 214 Seitenaufrufe, 108.564 Elemente,
4.126 Bedienelemente -- 0 Befunde. Alle vier Rollen, beide Breiten.
Zum ersten Mal.

pruef-werdegang 103/0 (war 100/3), pruef-nachfrage 51/0 (war 49),
pruef-stelle 15/0, pruef-entwicklung 48/0, pruef-start-ansicht 153/0,
pruef-wege-nach-draussen 67/0, pruef-tippziele 11/0, pruef-treff 80/0,
pruef-willkommen 67/0, pruef-sackgassen 13/0.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-22 10:14:25 +02:00

709 lines
31 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.
/* =====================================================================
DIE LAGE DES TEAMS -- eine Seite fuer die, die sie sehen duerfen.
Wunsch Filipe (10.09.2026): eine Stelle, an der er und VanVan alles
sehen und behandeln koennen.
SIE ZAEHLT NUR, SIE BEWERTET NICHT. Bewertet wird dort, wo die
Punkte stehen -- je Bereich fuehrt ein Verweis dorthin, mit der
Person schon vorausgewaehlt. Eine zweite Stelle zum Bewerten waere
eine zweite Stelle, an der es auseinanderlaeuft.
KEINE NEUEN CSS-KLASSEN. Fuer genau diese Art Karte gibt es sie
schon (`tperson`, `tz`, `schritt` -- benutzt von team.html). Elf
neue haetten in mehreren Stilvorlagen gepflegt werden muessen, fuer
ein Aussehen, das es bereits gibt. Zwei Bauarten fuer dieselbe Sache
sind ausserdem zwei Dinge zum Lernen statt einem.
IN DIESER DATEI STEHT KEIN ROLLENNAME. Sie wird wie jede Datei unter
/workspace an JEDEN ausgeliefert, der die Seite oeffnet. Wer sie
bekommt, entscheiden der Server (404 fuer alle anderen) und die
Kachel, die ebenfalls vom Server kommt.
===================================================================== */
(() => {
const $ = (id) => document.getElementById(id);
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 = (t) => { const f = $('fehler'); if (f) f.textContent = t || ''; };
let ich = null;
/* Die Stufen kommen mit der Antwort des Servers -- eine eigene Liste
hier waere die zweite, und die zweite laeuft irgendwann auseinander. */
let STUFEN = [];
const stufenName = (wert) =>
STUFEN.find((s) => s.wert === wert)?.name || STUFEN[0]?.name || '';
async function hole(pfad, optionen = {}) {
const a = await fetch(pfad, { credentials: 'same-origin', ...optionen });
if (a.status === 401) { location.assign('/workspace/'); throw new Error('abgemeldet'); }
if (a.status === 404) { location.assign('/workspace/start.html'); throw new Error('nicht erlaubt'); }
return a;
}
const tageHer = (iso) => {
if (!iso) return null;
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return null;
return Math.floor((Date.now() - d.getTime()) / 86400000);
};
/* Die drei Bereiche in derselben Reihenfolge wie auf der Startseite.
Die Namen stehen hier, weil die Seite sonst eine zweite Abfrage
braeuchte, nur um drei Woerter zu holen. */
const BEREICHE = [
{ schluessel: 'live', name: 'Live-Ablauf' },
{ schluessel: 'community', name: 'Community' },
{ schluessel: 'technik', name: 'Technik' },
];
function zahl(wert, schild) {
const k = el('div', 'tz');
k.append(el('strong', 'tz__wert', String(wert)));
k.append(el('span', 'tz__schild', schild));
return k;
}
function karte(m) {
const k = el('article', 'tperson');
/* DIE KARTE TRAEGT DIE FARBE DER STUFE (10.09.2026).
Vorher fiel `--r` auf `--text-still` zurueck -- ein helles Grau,
aus dem das Kantenlicht einen fahlen Schleier ueber die ganze
Karte legte. Auf dem Bildschirmfoto sah sie neben den beiden
dunklen Spalten daneben aus wie ueberbelichtet.
Die Stufe als Farbe zu nehmen loest zwei Dinge auf einmal: Der
Schleier wird zu einem klaren Ton, und man sieht schon am Rand
der Karte, wer wo steht -- ohne das Schild zu lesen. */
k.dataset.stufe = m.stufe || 'probe';
if (!m.aktiv) k.dataset.pause = 'ja';
/* ==== DIE RECHTE HAND SIEHT ANDERS AUS (20.09.2026) ==============
Filipe: "die kiste von rechte hand soll auch noch vieeeeeel
krasser und spezieller aussehen wie die von den modis."
ALS MERKMAL AM ELEMENT, nicht als eigene Bauweise: Die Karte
bleibt dieselbe Karte -- gleiche Felder, gleiche Reihenfolge,
gleiche Knoepfe. Nur das Aussehen aendert sich, und das
entscheidet das CSS. Zwei verschiedene Karten zu bauen hiesse,
jede kuenftige Aenderung zweimal zu machen; beim dritten Mal
vergisst man eine.
`rolle` kam bis heute gar nicht erst mit -- die Abfrage im
Server las sie nicht einmal.
KEIN RUECKFALL AUF EINEN ROLLENNAMEN (21.09.2026). Hier stand
`m.rolle || 'modi'`. Der Rueckfall war doppelt daneben:
(1) Er stand in einer Datei, die JEDER herunterlaedt, der den
Arbeitsplatz oeffnet -- auch jeder Manager, Scout und
Creator auf der Agenturadresse, und zwar OHNE Anmeldung.
Rollennamen der Community gehoeren dort nicht hin; darueber
wacht eine eigene Pruefung im Serverordner.
(2) Er hat nie etwas bewirkt. Nachgemessen: Die Stilvorlage
kennt zu dieser Karte nur `[data-rolle="hand"]`. Fuer jeden
anderen Wert sieht sie gleich aus -- ob dort ein Rollenname
steht oder nichts.
Der Server liefert `rolle` seit dem 20.09. immer mit (die
Abfrage holt sie, und sie ist auf zwei Werte begrenzt). Fehlt
sie doch einmal, bleibt das Merkmal weg statt falsch. */
if (m.rolle) k.dataset.rolle = m.rolle;
const kopf = el('div', 'tperson__kopf');
/* DAS BILD BEKOMMT SEINE KLASSE -- UND DAS WAR EIN ECHTER FEHLER.
Hier stand ein blankes <img> IN der Buchstaben-Blase. Ein Bild
ohne Groessenangabe nimmt seine natuerliche -- und die Blase hat
kein `overflow: hidden`. Ergebnis: Das Profilfoto lief ueber die
ganze Kachel, quer ueber Zahlen und Balken. Filipe hat es im
Bildschirmfoto gesehen, bevor ich es gemessen hatte.
`tperson__bild` gibt es laengst und traegt alles Noetige: 44 px,
rund, `object-fit: cover`, Rand in der Rollenfarbe. Sie steht
neben der Blase, nicht darin -- entweder ein Bild ODER ein
Buchstabe, nie beides ineinander. */
let zeichen;
if (m.bild) {
zeichen = document.createElement('img');
zeichen.className = 'tperson__bild';
zeichen.src = m.bild;
zeichen.alt = '';
zeichen.loading = 'lazy';
} else {
zeichen = el('span', 'tperson__zeichen');
zeichen.textContent = (m.name || '?').trim().charAt(0).toUpperCase();
}
kopf.append(zeichen);
const text = el('div', 'tperson__text');
text.append(el('p', 'tperson__namen', m.name));
/* WANN ZULETZT DA -- als Tatsache, nicht als Vorwurf. Wer zwei
Wochen nicht da war, hat vielleicht Urlaub; die Zahl sagt nur,
dass man nachfragen koennte. */
const her = tageHer(m.letzter_login);
text.append(el('span', 'tperson__zuletzt', her === null
? 'war noch nie angemeldet'
: her === 0 ? 'heute da gewesen'
: her === 1 ? 'gestern da gewesen' : `vor ${her} Tagen da gewesen`));
kopf.append(text);
k.append(kopf);
/* ===================================================================
DIE MITGLIEDER-KACHEL AUS KAPITEL 4 (10.09.2026)
Der Blueprint nennt dort Name und Foto, Zustaendigkeiten, den
Status (aktiv/pausiert), die Zahl offener Aufgaben, das Datum
des Eintritts und Schnellaktionen. Name, Foto und die Zahlen
standen schon; Status, "dabei seit" und die Stufe kommen hier
dazu.
================================================================ */
const merkmale = el('div', 'tperson__merkmale');
/* PAUSIERT ZUERST. Wer pausiert ist, erklaert jede andere Zahl
darunter -- null Aufgaben sind dann kein Grund nachzufragen. */
if (!m.aktiv) merkmale.append(el('span', 'tmerkmal tmerkmal--pause', 'pausiert'));
merkmale.append(el('span', `tmerkmal tmerkmal--${m.stufe || 'probe'}`,
stufenName(m.stufe)));
if (m.seit) {
const tage = tageHer(m.seit);
merkmale.append(el('span', 'tmerkmal tmerkmal--leise',
tage === null ? 'dabei'
: tage < 1 ? 'seit heute dabei'
: tage < 31 ? `seit ${tage} Tagen dabei`
: `seit ${Math.round(tage / 30.4)} Monaten dabei`));
}
k.append(merkmale);
/* ===================================================================
DIE VERFUEGBARKEITS-AMPEL (Blueprint 7.1)
"Owner/rechte Hand sehen nur die Verfuegbarkeits-Ampel, nicht den
Klartext." Sieben Tage, drei Farben, kein Wort darueber, was
jemand vorhat -- der Server schickt auch gar nichts anderes.
WOCHENTAGSBUCHSTABE STATT DATUM: Sieben Datumsangaben
nebeneinander liest niemand. Der Buchstabe reicht, um "Donnerstag
ist dicht" zu erkennen; das genaue Datum steht im Titel, den man
beim Draufzeigen bekommt.
DER TITEL SAGT NICHT, WAS ANLIEGT, sondern nur wie viel: "3
Termine, 2 Std 30". Das ist die Grenze aus dem Blueprint, und sie
laeuft hier mitten durch einen Satz -- deshalb steht sie hier
noch einmal. */
if (Array.isArray(m.verfuegbar) && m.verfuegbar.length) {
const woche = el('div', 'tampel');
/* DIE ÜBERSCHRIFT SAGTE „Kann heute" ÜBER SIEBEN TAGEN
(11.09.2026).
Darunter stand eine ganze Woche. Wer nur die Überschrift liest
-- und das tut man bei einer Zeile in Versalien --, hält den
ganzen Streifen für den heutigen Tag und liest sechs Kästchen
falsch. Der Satz DARUNTER war immer richtig; er spricht
ausdrücklich von heute. Zwei Aussagen über demselben Bild, und
die auffälligere war die falsche.
Gefunden nicht beim Lesen des Codes, sondern beim Nachmessen
am fertigen Bildschirm: sieben Kästchen, eine Überschrift im
Singular. */
woche.append(el('span', 'tampel__schild', 'Die nächsten sieben Tage'));
const streifen = el('div', 'tampel__woche');
m.verfuegbar.forEach((t, i) => {
const d = new Date(`${t.tag}T12:00:00Z`);
const tag = el('span', `tampel__tag tampel__tag--${t.ampel}`,
['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'][d.getUTCDay()]);
/* HEUTE WIRD MARKIERT. Ohne das sind es sieben gleich aussehende
Kästchen, und man muss den Wochentag im Kopf ausrechnen, um zu
wissen, wo man steht. Der erste Eintrag IST heute -- die
Abfrage im Server beginnt dort. */
if (i === 0) tag.dataset.heute = 'ja';
const dauer = t.minuten
? `${Math.floor(t.minuten / 60)} Std ${String(t.minuten % 60).padStart(2, '0')}`
: null;
tag.title = `${d.toLocaleDateString('de-DE', { weekday: 'long', day: 'numeric', month: 'long' })}`
+ (t.termine ? ` · ${t.termine} ${t.termine === 1 ? 'Termin' : 'Termine'}, ${dauer}` : ' · nichts eingetragen');
streifen.append(tag);
});
woche.append(streifen);
/* Der Satz sagt, was die Farben bedeuten -- einmal je Karte, nicht
als Legende irgendwo. Wer die Seite zum ersten Mal sieht, soll
nicht raten. */
const heuteWert = m.verfuegbar[0];
woche.append(el('span', 'tampel__satz',
heuteWert.ampel === 'frei' ? 'Heute nichts eingetragen – ansprechbar.'
: heuteWert.ampel === 'teils' ? 'Heute teilweise verplant.'
: 'Heute weitgehend verplant.'));
k.append(woche);
}
/* DIE STUFE SETZEN -- nur DogFather. Die rechte Hand sieht sie,
aendert sie aber nicht: Wer im Team auf welcher Stufe steht, ist
eine Personalentscheidung (Blueprint 4.1, "Rechte anpassen"
liegt beim Owner). Der Server sagt dasselbe noch einmal. */
if (ich?.rolle === 'admin') {
const wahl = el('div', 'tstufen');
wahl.append(el('span', 'tstufen__schild', 'Stufe'));
for (const st of STUFEN) {
const b = el('button', 'tstufen__knopf', st.name);
b.type = 'button';
b.dataset.stufe = st.wert;
if ((m.stufe || STUFEN[0]?.wert) === st.wert) b.dataset.an = 'ja';
b.addEventListener('click', () => stufeSetzen(wahl, m, st.wert));
wahl.append(b);
}
k.append(wahl);
}
/* SCHNELLAKTION: schreiben. `?mit=` legt das Gespraech beim ersten
Mal an und findet beim zweiten dasselbe wieder -- der Weg gibt es
seit dem 06.09. und wird hier nur benutzt, nicht neu gebaut.
Nachgemessen: Die rechte Hand und die Modis duerfen einander
schreiben, ein Manager ist fuer beide nicht erreichbar. */
/* EIN KNOPF, KEIN UNTERSTRICHENER SATZ (11.09.2026). Die Klasse
`schritt` zeichnet einen Textweg -- über die volle Breite einer
Karte gezogen sah er aus wie eine Überschrift mit Unterstrich.
`tschreiben` ist derselbe Weg, nur als Knopf erkennbar. */
const aktionen = el('div', 'tpipe__reihe');
const schreiben = el('a', 'tschreiben', `${m.name} schreiben`);
schreiben.href = `chat.html?mit=${m.id}`;
aktionen.append(schreiben);
k.append(aktionen);
const zahlen = el('div', 'tperson__zahlen');
zahlen.append(zahl(m.aufgaben.offen, 'Aufgaben offen'));
zahlen.append(zahl(m.aufgaben.ueberfaellig, 'überfällig'));
zahlen.append(zahl(m.ideen, 'Ideen'));
zahlen.append(zahl(m.nachrichten, 'Rückmeldungen'));
k.append(zahlen);
/* ===================================================================
DIE DREI BEREICHE ALS BALKEN (10.09.2026)
Hier standen bis eben drei weitere Zahlenkaesten ("40 Live-Ablauf:
offen") und darunter noch einmal drei Verweise mit denselben
Zahlen in Klammern. Sechs Kaesten fuer drei Aussagen -- und die
wichtigste stand in keinem davon: WIE WEIT sind wir?
"40 offen" beantwortet das nicht. Vierzig von vierzig ist nicht
dasselbe wie vierzig von hundert, und genau das ist die Frage,
die man beim Hinsehen hat.
Ein Balken beantwortet sie ohne Rechnen: gut und "verbessern"
fuellen ihn von links, der Rest ist offen. Zwei Farben, weil es
zwei verschiedene Dinge sind -- "besprochen und in Ordnung" und
"besprochen, da ist was". Beide sind ERLEDIGT im Sinne von
"angesehen", und deshalb fuellen beide den Balken.
Die ganze Zeile ist der Verweis. Vorher war es ein eigener Knopf
darunter; ein Balken, der aussieht wie eine Anzeige, aber klickbar
ist, muss auch wie ein Ziel aussehen -- deshalb Rahmen und
Hover. */
const fortschritt = el('div', 'tfort');
for (const b of BEREICHE) {
const z = m.bereiche[b.schluessel];
if (!z) continue;
const gesamt = Math.max(z.gesamt || 0, 1);
const beurteilt = (z.gut || 0) + (z.verbessern || 0);
const a = el('a', 'tfort__zeile');
a.href = `bereich.html?b=${b.schluessel}&creator_id=${m.id}`;
const kopfz = el('span', 'tfort__kopf');
kopfz.append(el('span', 'tfort__name', b.name));
kopfz.append(el('span', 'tfort__zahl', `${beurteilt} von ${z.gesamt}`));
a.append(kopfz);
const bahn = el('span', 'tfort__bahn');
const gut = el('i', 'tfort__gut');
gut.style.width = `${((z.gut || 0) / gesamt) * 100}%`;
const schlecht = el('i', 'tfort__verbessern');
schlecht.style.width = `${((z.verbessern || 0) / gesamt) * 100}%`;
bahn.append(gut, schlecht);
a.append(bahn);
/* Die Aufschluesselung in Worten -- fuer alle, die den Balken
nicht sehen koennen, und fuer die Faelle, in denen zwei
Prozentpunkte Unterschied nicht auffallen. */
a.append(el('span', 'tfort__unter',
z.verbessern
? `${z.gut} in Ordnung · ${z.verbessern} zu besprechen · ${z.offen} offen`
: `${z.gut} in Ordnung · ${z.offen} offen`));
fortschritt.append(a);
}
if (fortschritt.children.length) k.append(fortschritt);
/* UND HIER WIRD BEHANDELT. Ein Verweis je Bereich, der die Person
mitnimmt -- ohne sie muesste man dort noch einmal auswaehlen,
und beim zweiten Mal weiss man nicht mehr, warum man hier war. */
return k;
}
/* =====================================================================
DER EINGANG -- was die Modis gemeldet haben.
Er steht OBEN, vor den Karten: Das ist das, was eine Antwort
braucht. Die Zahlen darunter kann man sich ansehen; hier wartet
jemand.
ZWEI ANTWORTEN, und beide kommen bei ihm an. Eine Meldung, auf
die nichts folgt, sieht fuer ihn aus wie eine, die niemand gelesen
hat -- und beim dritten Mal meldet er nichts mehr.
===================================================================== */
/* ===================================================================
DIE STUFE SETZEN
Sofort sichtbar, dann speichern. Andersherum -- erst der Server,
dann die Anzeige -- fuehlt sich bei jedem Druck traege an, obwohl
nichts langsam ist. Schlaegt es fehl, springt die Auswahl zurueck
UND es steht eine Meldung da: Eine Anzeige, die sich lautlos
zurueckdreht, sieht wie ein Wackelkontakt aus.
================================================================ */
async function stufeSetzen(wahl, m, stufe) {
const knoepfe = [...wahl.querySelectorAll('.tstufen__knopf')];
const vorher = knoepfe.find((b) => b.dataset.an === 'ja')?.dataset.stufe || null;
if (vorher === stufe) return;
const zeigen = (wert) => {
for (const b of knoepfe) {
if (b.dataset.stufe === wert) b.dataset.an = 'ja';
else delete b.dataset.an;
}
};
zeigen(stufe);
for (const b of knoepfe) b.disabled = true;
try {
const a = await hole('/workspace/api/teamlage/stufe', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ person_id: m.id, stufe }),
});
if (!a.ok) throw new Error('abgelehnt');
m.stufe = stufe;
/* Das Schild oben in derselben Karte zieht mit -- sonst stuende
dort die alte Stufe neben der neuen Auswahl. */
const schild = wahl.parentElement?.querySelector('[class*="tmerkmal--"]:not(.tmerkmal--pause):not(.tmerkmal--leise)');
if (schild) {
schild.className = `tmerkmal tmerkmal--${stufe}`;
schild.textContent = stufenName(stufe);
}
melde('');
} catch {
zeigen(vorher);
melde('Die Stufe konnte nicht gespeichert werden.');
} finally {
for (const b of knoepfe) b.disabled = false;
}
}
function eingangBauen(liste) {
const k = el('section', 'tperson');
k.append(el('p', 'feldschild', 'Rückmeldungen aus dem Team'));
if (!liste.length) {
k.append(el('p', 'tperson__zuletzt',
'Nichts offen – auf alles, was gemeldet wurde, habt ihr geantwortet.'));
return k;
}
for (const m of liste) {
const zeile = el('div', 'tpipe');
zeile.append(el('p', 'tperson__namen', m.titel));
if (m.begruendung) zeile.append(el('p', 'tperson__unter', m.begruendung));
zeile.append(el('span', 'tperson__zuletzt',
`${m.von || 'jemand'} · ${(m.am || '').slice(0, 10)}`));
const knoepfe = el('div', 'tpipe__reihe');
const melden = (t) => { melde(t); };
const ja = el('button', 'schritt', 'Daraus wird eine Aufgabe');
ja.type = 'button';
ja.addEventListener('click', async () => {
ja.disabled = true;
try {
const a = await hole('/workspace/api/teamlage/behandeln', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
bereich: m.bereich, schluessel: m.schluessel,
person_id: m.person_id, art: 'aufgabe',
}),
});
if (!a.ok) {
melden(sagWas((await a.json().catch(() => ({}))).fehler, 'Das hat nicht geklappt.'));
ja.disabled = false; return;
}
melden('');
await laden();
} catch { ja.disabled = false; }
});
knoepfe.append(ja);
const nein = el('button', 'abmelden', 'Nicht nötig');
nein.type = 'button';
nein.addEventListener('click', async () => {
/* EINE ABSAGE OHNE GRUND IST KEINE ANTWORT. Deshalb wird
gefragt -- und ein leeres Feld bricht ab, statt still nichts
zu tun.
WAR BIS ZUM 22.09.2026 EIN window.prompt(). Der letzte im
Haus, und er hatte drei Nachteile, die man erst am Handy
sieht: Er haelt die ganze Seite an, er sieht auf jedem
Browser anders aus als der Rest der Oberflaeche, und sein
Text laesst sich nicht gliedern -- Titel, Erklaerung und
Feld standen als ein Absatz untereinander.
`frageNach` ist derselbe Dialog wie an den 42 anderen
Stellen. Er kann ausserdem etwas, das prompt() nicht kann:
sagen, was BLEIBT, wenn man absagt. Genau das ist hier die
Frage, die jemand vor dem Klicken hat. */
const antwort = await window.frageNach({
titel: 'Nicht nötig?',
was: '„' + m.titel + '“',
bleibt: 'Die Meldung verschwindet aus der Liste. Dein Grund steht '
+ 'dabei, damit spaeter nachvollziehbar ist, warum.',
ja: 'Absagen',
grund: 'Warum machen wir das nicht?', grundPflicht: true,
platzhalter: 'z. B. Haben wir letzte Woche schon anders geloest.',
});
if (!antwort) return;
const grund = antwort.grund || '';
if (!grund.trim()) { melden('Ohne Grund bleibt die Meldung stehen.'); return; }
nein.disabled = true;
try {
const a = await hole('/workspace/api/teamlage/behandeln', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
bereich: m.bereich, schluessel: m.schluessel,
person_id: m.person_id, art: 'abgelehnt', grund: grund.trim(),
}),
});
if (!a.ok) {
melden(sagWas((await a.json().catch(() => ({}))).fehler, 'Das hat nicht geklappt.'));
nein.disabled = false; return;
}
melden('');
await laden();
} catch { nein.disabled = false; }
});
knoepfe.append(nein);
zeile.append(knoepfe);
k.append(zeile);
}
return k;
}
/* =====================================================================
DIE ANGEBOTE -- Vorschlaege, ueber die ihr entscheidet.
Sie stehen im selben Eingang wie die Rueckmeldungen: EIN Ort, an
dem etwas auf eine Antwort wartet. Wer zwei Eingaenge hat, sieht
irgendwann in den falschen.
===================================================================== */
const AUFWAND = { hoch: 'viel Aufwand', mittel: 'mittlerer Aufwand', niedrig: 'wenig Aufwand' };
async function entscheiden(knopf, koerper) {
knopf.disabled = true;
try {
const a = await hole('/workspace/api/teamlage/angebot', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(koerper),
});
if (!a.ok) {
melde(sagWas((await a.json().catch(() => ({}))).fehler, 'Das hat nicht geklappt.'));
knopf.disabled = false; return;
}
const d = await a.json().catch(() => ({}));
melde(d.aufgaben ? (d.aufgaben === 1 ? 'Eine Aufgabe angelegt.' : `${d.aufgaben} Aufgaben angelegt.`) : '');
await laden();
} catch { knopf.disabled = false; }
}
function angeboteBauen(liste) {
const k = el('section', 'tperson');
k.append(el('p', 'feldschild', 'Angebote aus dem Team'));
if (!liste.length) {
k.append(el('p', 'tperson__zuletzt', 'Gerade liegt kein Vorschlag zur Entscheidung.'));
return k;
}
for (const a of liste) {
const zeile = el('div', 'tpipe');
zeile.append(el('p', 'tperson__namen', a.titel));
if (a.text) zeile.append(el('p', 'tperson__unter', a.text));
/* WAS ES FUER DICH HEISST, steht direkt dabei -- sonst muss man
die Entscheidung treffen, ohne den eigenen Teil zu kennen. */
if (a.einsatz) {
zeile.append(el('p', 'tperson__unter', `Dein Teil: ${a.einsatz}`));
}
zeile.append(el('span', 'tperson__zuletzt',
`${a.von_name || 'jemand'} · Nutzen ${a.bewertung ?? '–'}/5 · `
+ `${AUFWAND[a.dringlichkeit] || a.dringlichkeit}`));
const knoepfe = el('div', 'tpipe__reihe');
const ja = el('button', 'schritt', 'Annehmen');
ja.type = 'button';
ja.addEventListener('click', () => entscheiden(ja, { id: a.id, art: 'angenommen' }));
knoepfe.append(ja);
const nein = el('button', 'abmelden', 'Nicht umsetzen');
nein.type = 'button';
nein.addEventListener('click', async () => {
/* Derselbe Dialog wie ueberall sonst (22.09.2026). Vorher ein
window.prompt() -- der zweite und letzte im Haus. */
const antwort = await window.frageNach({
titel: 'Nicht umsetzen?',
was: '„' + a.titel + '“',
bleibt: 'Das Angebot verschwindet aus der Liste. Dein Grund steht '
+ 'dabei, damit die Person weiss, woran es lag.',
ja: 'Nicht umsetzen',
grund: 'Warum nicht?', grundPflicht: true,
platzhalter: 'z. B. Passt gerade nicht zum Plan – vielleicht spaeter.',
});
if (!antwort) return;
const grund = antwort.grund || '';
if (!grund.trim()) { melde('Ohne Grund bleibt das Angebot stehen.'); return; }
entscheiden(nein, { id: a.id, art: 'abgelehnt', grund: grund.trim() });
});
knoepfe.append(nein);
zeile.append(knoepfe);
k.append(zeile);
}
return k;
}
async function laden() {
const ziel = $('lage');
try {
const a = await hole('/workspace/api/teamlage');
if (!a.ok) { melde('Die Lage konnte nicht geladen werden.'); return; }
const daten = await a.json();
/* VOR dem Bauen der Karten: Sie fragen die Liste ab. Stuende
diese Zeile darunter, waere die Stufe beim ersten Aufbau leer
und erst nach dem naechsten Laden da -- ein Fehler, den man
beim Entwickeln nie sieht, weil man ohnehin neu laedt. */
STUFEN = Array.isArray(daten.stufen) && daten.stufen.length ? daten.stufen : STUFEN;
ziel.textContent = '';
ziel.setAttribute('aria-busy', 'false');
const leute = daten.leute || [];
const offenGesamt = leute.reduce((s, m) =>
s + Object.values(m.bereiche).reduce((x, b) => x + b.offen, 0), 0);
/* Eine leere Seite ohne Erklaerung sieht aus wie ein Fehler --
deshalb steht hier auch, was zu tun waere. */
const wartend = (daten.eingang || []).length + (daten.angebote || []).length;
$('kopf-unter').textContent = leute.length
? `${leute.length} ${leute.length === 1 ? 'Person' : 'Personen'} im Team`
: 'Noch niemand im Team. Zugänge legst du unter „Personen & Zugänge“ an.';
/* ===================================================================
DIE DREI ZAHLEN, DIE DIE SEITE BEANTWORTET (10.09.2026)
Sie standen als Fliesstext in einer Zeile: "1 Person · 0
Rueckmeldungen warten · 101 Punkte noch nicht beurteilt".
Genau das sind aber die drei Fragen, wegen derer man diese
Seite ueberhaupt aufmacht -- und in einem Satz muss man sie
lesen statt zu sehen.
DIE DRITTE BEKOMMT EINEN BALKEN. "101 noch nicht beurteilt"
beantwortet nicht, wie weit man ist: Hundertein von hundertzwei
ist etwas anderes als hundertein von dreihundert. Der Balken
beantwortet es ohne Rechnen.
BEI NIEMANDEM IM TEAM bleibt die Leiste ganz weg. Drei Nullen
mit Balken auf null sehen aus wie ein Fehler; der Satz oben
sagt dann, was zu tun ist. */
const punkteGesamt = leute.reduce((sum, m) =>
sum + Object.values(m.bereiche).reduce((x, b) => x + (b.gesamt || 0), 0), 0);
const beurteilt = Math.max(punkteGesamt - offenGesamt, 0);
const pausiert = leute.filter((m) => !m.aktiv).length;
if (leute.length) {
const leiste = el('div', 'tkopfzahlen');
const eins = el('div', 'tkz');
eins.append(el('strong', 'tkz__wert', String(leute.length)));
eins.append(el('span', 'tkz__schild',
pausiert ? `im Team · ${pausiert} pausiert` : 'im Team'));
leiste.append(eins);
const zwei = el('div', 'tkz');
if (wartend) zwei.dataset.dran = 'ja';
zwei.append(el('strong', 'tkz__wert', String(wartend)));
zwei.append(el('span', 'tkz__schild',
wartend === 1 ? 'wartet auf dich' : 'warten auf dich'));
leiste.append(zwei);
const drei = el('div', 'tkz');
drei.append(el('strong', 'tkz__wert', `${beurteilt}`));
drei.append(el('span', 'tkz__schild', `von ${punkteGesamt} Punkten besprochen`));
const bahn = el('span', 'tkz__bahn');
const voll = el('i', '');
voll.style.width = `${punkteGesamt ? (beurteilt / punkteGesamt) * 100 : 0}%`;
bahn.append(voll);
drei.append(bahn);
leiste.append(drei);
ziel.append(leiste);
}
/* ===================================================================
ZWEI REIHEN STATT EINER (10.09.2026)
Vorher lagen alle fuenf Kaesten in EINEM Raster: die beiden
Wartelisten und je eine Karte pro Person. Zwei Folgen, beide
schlecht:
* DIE SPALTEN WURDEN ZU SCHMAL. Bei fuenf nebeneinander blieb
jeder rund 150 px, und im Bildschirmfoto stand "Community
12 von" -- abgeschnitten mitten in der Zahl.
* DIE LAENGSTE BESTIMMTE DIE HOEHE ALLER. Zehn Rueckmeldungen
machten die Reihe so hoch wie zehn Rueckmeldungen; daneben
standen drei kurze Karten in sehr viel Luft.
Es sind auch zwei verschiedene Dinge: oben, was eine ANTWORT
braucht, unten, wer im Team ist. Getrennte Reihen sagen das,
ohne es hinzuschreiben. */
const wartet = el('div', 't-wartet');
wartet.append(eingangBauen(daten.eingang || []));
wartet.append(angeboteBauen(daten.angebote || []));
ziel.append(wartet);
const runde = el('div', 't-leute');
for (const m of leute) runde.append(karte(m));
ziel.append(runde);
} catch { /* umgeleitet */ }
}
(async () => {
try {
const a = await hole('/workspace/api/ich');
if (!a.ok) { location.assign('/workspace/'); return; }
/* Wer zusieht, entscheidet, ob die Stufenwahl erscheint. Die
Antwort wurde bisher nur weitergereicht -- gemerkt wird sie
erst, seit es hier etwas gibt, das nicht jeder darf.
DER SERVER ENTSCHEIDET TROTZDEM: Diese Zeile blendet nur aus.
Wer den Aufruf von Hand nachbaut, bekommt 404 -- das steht in
workspace-teamlage.js und nicht hier. */
ich = await a.json();
window.werZeigen?.(ich);
} catch {
/* NICHT MEHR STUMM (20.09.2026) -- siehe meldung.js. */
window.ladefehler?.();
return;
}
$('abmelden')?.addEventListener('click', async () => {
try { await fetch('/workspace/api/abmelden', { method: 'POST', credentials: 'same-origin' }); }
catch { /* egal */ }
location.assign('/workspace/');
});
await laden();
})();
})();