Filipe: "auch die hintergrund bilder vom crew espace sollen anderes
aussehen, der community angepasst wie die zugangscode seite die ist
mega."
UMFAERBEN HAETTE HIER NICHT GEREICHT. Bei der Zugangswand war Rot das
Problem; innen zeigen die neun Buehnen das SPICY-MEDIA-LOGO selbst, in
`halle` und `arena` vielfach nebeneinander. Ein blaues fremdes Logo
waere schlimmer gewesen als ein rotes.
Die Motive gibt es laengst -- auf der oeffentlichen Dogfather-Seite:
studio/lounge "Team Dogi -- Familie. Treue. Zusammenhalt."
halle TEAM DOGI mit den Menschen des Teams
skyline/portal "Streamplan": Dogi und HasiDog auf der Buehne
arena "DogFathers Galerie": Filmrollen, ein Archiv
garage "Der Streamer": Filipe an seinem Platz
Fuer `garage` stand zuerst der Holo-Kontrollraum da -- grossartiges
Bild, aber darauf steht "WERDE MODI". Eine Einladung hinter dem
Aufgabenbrett von Leuten, die laengst dabei sind, liest sich falsch.
Ein Bild sagt etwas, auch wenn es nur Hintergrund ist.
DIE ABDUNKLUNG IST GERECHNET, NICHT GEWAEHLT
Mein erster fester Wert haette 18 von 27 Fassungen HELLER gemacht als
die, die sie ersetzen (114 gegen 67) -- ueber ihnen steht derselbe
kleine Text. Das Werkzeug misst jetzt jede Fassung gegen die bestehende
Buehne derselben Szene und senkt sie genau auf deren Wert.
Ueber eine KURVE statt eines schwarzen Schleiers: v' = 255*(v/255)^g
senkt die Lichter viel staerker als die Tiefen -- das Bild wird dunkler
und behaelt seine Zeichnung. Ein Schleier haette bei den hellsten
Motiven ueber 80 % gebraucht, und darunter liegt dann Nebel statt Bild
(derselbe Fehler wie beim Tresorbild).
Der erste Anlauf mit einer geschlossenen Formel war falsch: Die Kurve
wirkt auf jeden FARBKANAL, die Helligkeit ist eine gewichtete Summe der
drei, und L(v^g) ist nicht L(v)^g. Die Rechnung sagte 61,5 und heraus
kamen 67. Jetzt wird genaehert und nachgemessen, hoechstens sechsmal.
Beim Hochkant-Ausschnitt wird zusaetzlich das FENSTER GESUCHT: fuenf
Kandidaten, erst "dunkel genug", dann "am meisten Motiv". Die Mitte --
die das Haus sonst nimmt -- ist bei diesen Bildern das Hellste.
ZWEI FUNDE AUS DEM BILDSCHIRMFOTO, die nichts mit den Buehnen zu tun
hatten:
1. DIE KOPFLEISTE sagte "SPICY & DOGI · CREATOR WORKSPACE". Reitertitel,
Begruessung und Symbol waren laengst richtig -- nur die Zeile, die
man als Erstes ansieht, nicht. Der Grund: Auf Unterseiten traegt die
Leiste einen Rueckweg-Verweis, auf der Startseite blossen Text; mein
Code kannte nur die erste Bauform.
2. DAS CHILI-ZEICHEN kommt aus dem CSS als Bild, an drei Stellen
(Kopfleiste, Siegel im Ring, die schwebenden Wasserzeichen). Auf
crew. wird die Datei serverseitig durch den Husky ersetzt -- alle
drei auf einmal, und ohne Flackern, weil schon der erste Abruf das
richtige Bild bekommt.
GEMESSEN
pruef-crew-adresse 123 (statt 117): Fuer JEDE Buehne aus start.css --
die Namen werden dort GELESEN, nicht abgeschrieben --
gibt es auf crew. ein eigenes Motiv, auf workspace.
das alte, und die beiden sind nachweislich
verschiedene Dateien.
pruef-rollen 274 pruef-start-ansicht, pruef-zwischenspeicher 21
pruef-modi-wortleck 5 -- er hat wieder angeschlagen, zweimal auf
Kommentare, die ich selbst geschrieben hatte.
27 Fassungen, 3,5 MB (die bisherigen neun Szenen brauchen 7,1).
Co-Authored-By: Claude Opus 5 <[email protected]>
565 lines
23 KiB
JavaScript
565 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 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 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();
|
||
})();
|
||
})();
|