Files
dogfather-universe/workspace/assets/js/teamlage.js
T
DogFatherGitandClaude Opus 5 8bcbd83061 Die Modi-Seite gehoert Dogi und den Modis -- Wand, Zeichen, Reiter
Filipe zum Bildschirmfoto der Zugangswand auf crew.: "alles soll auf
dogfather und die modis perfektionniert werden auf der neuen modi seite."

Dort stand bis eben die gewohnte Wand: Spicy-Media-Logo, "Creator
Workspace", der Satz ueber Manager, Scouts und Creator, fuenf
Rollenkacheln. Fuer einen Modi war davon nichts richtig.

DIE KACHELREIHE FAELLT WEG, und das ist eine Verbesserung: Eine Reihe mit
genau einem Eintrag ist keine Auswahl, sondern eine Huerde -- man muesste
erst daraufdruecken, bevor das Codefeld etwas annimmt. gate.js prueft
jetzt, OB es Kacheln gibt, statt sie vorauszusetzen.

DIE BUEHNE IST AUS DEM VORHANDENEN MOTIV GEBAUT, nicht neu erfunden, und
das ist kein Sparen: Die Anmeldeseite ist eine MECHANIK. Rechts steht im
Bild eine leere Tafel, und gate.css setzt die Karte auf Hundertstel genau
dort hinein. Ein frei erfundenes Bild haette diese vier Zahlen
mitgenommen. Also dieselbe Szene, ueber den Mischmodus "color" ins Blau
umgefaerbt (hue-rotate haette Rot nach Cyan UND Blau nach Gelb gedreht),
Dogi anstelle des Spicy-Medaillons, "TEAM DOGI" darunter.
Gemessen: rote Bildpunkte 18,1 % -> 0,0 %, Karte auf 0 px genau.

NACH DER ANMELDUNG geht es weiter: kopf.js zieht Reitertitel und Zeichen
aus `ich.marke` nach -- "Aufgaben · Team Dogi" statt "· Spicy & Dogi", mit
dem eigenen Symbol. An einer Stelle statt in 30 HTML-Dateien.

DREI FUNDE, DIE NICHT AUS DEM KOPF KAMEN

1. DER KNOPF WAERE SCHLECHTER LESBAR GEWORDEN. Mein erstes Blau endete
   bei #4aa4cf -- weisse Schrift darauf: 2,8 zu 1. Der rot-blaue Verlauf,
   den er ersetzt, haelt ueber seine GANZE Laenge 4,65; er war offenbar
   genau darauf gebaut. Jetzt 5,10 zu 1, am fertigen Bildschirmfoto
   gemessen statt aus einer einzelnen Farbe hergeleitet.

2. DIE NEUE WAND WAR AUCH AUF workspace. ABRUFBAR. Sie liegt als Datei im
   selben Ordner. Aufgefallen ist das, weil der Wortleck-Test sie ueberhaupt
   las -- die Frage WARUM war die Antwort. Ausserhalb von crew. antwortet
   sie jetzt mit 404; auf den Pruefadressen bleibt sie erreichbar, sonst
   koennte die Pruefung sie nicht mehr oeffnen und waere gruen ohne etwas
   zu messen.

3. ZWEI GLEICHE KACHELN AUF DER STARTSEITE, seit dem letzten Deploy live.
   Die Team-Lage-Kachel von heute Mittag hatte Name, Zeichen, Ton UND
   Gruppe einer schon vorhandenen -- fuehrte aber woandershin. Gefunden
   von "jede Kachel hat ihre eigene Farbe (23 Farben auf 24 Kacheln)".
   Jetzt "Eingang", Gruppe Taeglich, neuer Ton 24 (#8a20cf, Abstand 38,5
   im CIELAB-Raum; reines Blau haette 52 gehabt und waere auf dunklem
   Grund am schlechtesten zu fokussieren). Eine Fehlermeldung zeigte
   dadurch auf die falsche Seite -- ebenfalls behoben.
   Und die Pruefung, die es fand, zaehlte nur bereiche.js: Vom Server
   angehaengte Kacheln kannte sie nicht. Sie fragt jetzt beide Quellen.

Der Wortleck-Test meldete ausserdem sieben Fundstellen -- allesamt
Kommentare, die ich beim Bauen selbst geschrieben hatte.

GEMESSEN
pruef-crew-adresse     103   pruef-crew-wand-bild    43 (neu)
pruef-modi-checkliste   59   pruef-modi-verborgen    75
pruef-rollen           245   pruef-zwischenspeicher  21
pruef-modi-wortleck      4   pruef-start-ansicht     gruen

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-10 13:59:54 +02:00

317 lines
12 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 || ''; };
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');
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);
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'));
for (const b of BEREICHE) {
const z = m.bereiche[b.schluessel];
if (z) zahlen.append(zahl(z.offen, `${b.name}: offen`));
}
k.append(zahlen);
/* 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. */
const wege = el('div', 'tpipe__reihe');
for (const b of BEREICHE) {
const z = m.bereiche[b.schluessel];
if (!z) continue;
const a = el('a', 'schritt',
`${b.name} ansehen (${z.gut} gut · ${z.verbessern} verbessern)`);
a.href = `bereich.html?b=${b.schluessel}&creator_id=${m.id}`;
wege.append(a);
}
k.append(wege);
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.
===================================================================== */
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();
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'} · `
+ `${wartend} ${wartend === 1 ? 'Rückmeldung wartet' : 'Rückmeldungen warten'} · `
+ `${offenGesamt} Punkte noch nicht beurteilt`
: 'Noch niemand im Team. Zugänge legst du unter „Personen & Zugänge“ an.';
ziel.append(eingangBauen(daten.eingang || []));
ziel.append(angeboteBauen(daten.angebote || []));
for (const m of leute) ziel.append(karte(m));
} catch { /* umgeleitet */ }
}
(async () => {
try {
const a = await hole('/workspace/api/ich');
if (!a.ok) { location.assign('/workspace/'); return; }
window.werZeigen?.(await a.json());
} catch { return; }
$('abmelden')?.addEventListener('click', async () => {
try { await fetch('/workspace/api/abmelden', { method: 'POST', credentials: 'same-origin' }); }
catch { /* egal */ }
location.assign('/workspace/');
});
await laden();
})();
})();