Zwei Auftraege in einem Zug: "mach weiter" (Blueprint Kapitel 4/4.1)
und, zum Bildschirmfoto der Seite, "das muss viel krasser sein".
KAPITEL 4 -- DIE MITGLIEDER-KACHEL
Der Blueprint nennt: "Name & Foto · Rolle(n)/Kategorie(n) · Status
(aktiv/pausiert) · Anzahl offener Aufgaben · Datum 'Modi seit' ·
Schnellaktionen". Name, Foto und Zahlen standen schon; Status, "dabei
seit", Stufe und ein Knopf zum Schreiben kommen dazu.
KAPITEL 4.1 -- DIE DREI STUFEN
Probe, Standard, Senior. Sie sind eine ARBEITSEINTEILUNG, keine
Rechtegrenze -- was ein Modi darf, haengt an der Rolle; die Stufe sagt,
wo er im Team steht. Gesetzt werden sie nur von DogFather: Der
Blueprint gibt der rechten Hand den gleichen UEBERBLICK, aber
"Verwaltungsrechte optional durch Owner freischaltbar", also aus.
NULL heisst "Probe" und nicht "unbekannt" -- ein dritter Zustand waere
eine Frage, die niemand beantworten kann.
ZWEI ECHTE FEHLER, BEIDE VON DER NEUEN PRUEFUNG GEFUNDEN
1. PAUSIERTE VERSCHWANDEN KOMPLETT. In der Abfrage stand `AND aktiv =
1`. Wer jemanden pausierte, bei dem verschwand er samt seiner
OFFENEN RUECKMELDUNGEN aus dem Eingang -- die warteten weiter auf
eine Antwort, nur sah sie niemand mehr.
2. DER EINGANG HAETTE AUF FRISCHER ANLAGE 503 GELIEFERT. Die
Checklisten-Tabellen entstehen beim ersten Aufruf einer Checkliste;
diese Seite liest sie aber auch. Wer sie oeffnete, bevor je jemand
eine Checkliste angesehen hatte, bekam einen Fehler ohne Erklaerung
-- auf einer neuen Anlage also beim allerersten Blick. Das Schema
hat jetzt einen Besitzer, der es herausgibt; ein zweites CREATE
TABLE waere der Anfang von zwei Schemata gewesen.
"VIEL KRASSER" -- UND ZWAR MIT INFORMATION, NICHT MIT LAERM
* ZWEI REIHEN STATT EINER. Alle fuenf Kaesten lagen in EINEM Raster
mit 150 px Mindestbreite: Im Bildschirmfoto stand "Community 12
von" -- abgeschnitten mitten in der Zahl -- und die laengste Liste
machte die ganze Reihe so hoch wie sich selbst. Jetzt oben, was
eine Antwort braucht, darunter das Team; 300 px Mindestbreite.
* DIE KARTE WAR FAHL, und das war ein Fehler: `--r` fiel auf ein
helles Grau zurueck, aus dem das Kantenlicht einen Nebel ueber die
ganze Karte legte. Sie traegt jetzt die Farbe der Stufe -- kein
Nebel, und man sieht am Rand, wer wo steht.
* SECHS ZAHLENKAESTEN WURDEN DREI BALKEN. "40 offen" beantwortet
nicht, wie weit man ist: 40 von 40 ist etwas anderes als 40 von
100. Gruen (in Ordnung) und Bernstein (zu besprechen) fuellen den
Balken; die ganze Zeile ist der Weg dorthin.
* DIE DREI ZAHLEN DER SEITE stehen oben als Zahlen statt in einem
Satz. Die Warnfarbe erscheint NUR, wenn wirklich etwas wartet --
eine Null in Bernstein waere ein Alarm ohne Anlass, und ab dem
dritten Mal sieht man ihn nicht mehr.
GEMESSEN
pruef-team-stufen (neu) 24, mit Gegenprobe: hoch- und zurueckstufen,
und dieselbe Stufe zweimal zu setzen darf
keine zweite Protokollzeile erzeugen
pruef-rollen 274 pruef-modi-checkliste 59
pruef-zwischenspeicher 21
Ansicht: Rechner 1440 und Handy 412 -- keine Skriptfehler, nichts
ragt seitlich heraus (0 px).
Co-Authored-By: Claude Opus 5 <[email protected]>
564 lines
23 KiB
JavaScript
564 lines
23 KiB
JavaScript
/* =====================================================================
|
||
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';
|
||
|
||
const kopf = el('div', 'tperson__kopf');
|
||
const zeichen = el('span', 'tperson__zeichen');
|
||
if (m.bild) {
|
||
const b = document.createElement('img');
|
||
b.src = m.bild; b.alt = ''; b.loading = 'lazy';
|
||
zeichen.append(b);
|
||
} else {
|
||
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 & Foto · Rolle(n)/Kategorie(n) ·
|
||
Status (aktiv/pausiert) · Anzahl offener Aufgaben · Datum 'Modi
|
||
seit' · 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 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. */
|
||
const aktionen = el('div', 'tpipe__reihe');
|
||
const schreiben = el('a', 'schritt', `${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((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. */
|
||
const grund = window.prompt(
|
||
`Warum machen wir das nicht?
|
||
|
||
„${m.titel}“`, '');
|
||
if (grund === null) return;
|
||
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((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((await a.json().catch(() => ({}))).fehler || 'Das hat nicht geklappt.');
|
||
knopf.disabled = false; return;
|
||
}
|
||
const d = await a.json().catch(() => ({}));
|
||
melde(d.aufgaben ? `${d.aufgaben} Aufgabe(n) 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', () => {
|
||
const grund = window.prompt(`Warum nicht?
|
||
|
||
„${a.titel}“`, '');
|
||
if (grund === null) return;
|
||
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.modis || [];
|
||
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 { return; }
|
||
$('abmelden')?.addEventListener('click', async () => {
|
||
try { await fetch('/workspace/api/abmelden', { method: 'POST', credentials: 'same-origin' }); }
|
||
catch { /* egal */ }
|
||
location.assign('/workspace/');
|
||
});
|
||
await laden();
|
||
})();
|
||
})();
|