Files
dogfather-universe/workspace/assets/js/talente.js
T
DogFatherGitandClaude Opus 5 8e140724f2 Der Rollenname steht in keiner ausgelieferten Datei mehr
pruef-modi-wortleck war rot und meldete 17 Fundstellen -- seit Wochen,
bei jedem Lauf. Eine Warnung, die immer kommt, wird ueberlesen, und
dann auch die echte.

ERST GEMESSEN, OB DIE REGEL UEBERHAUPT NOCH GILT. Seit dem 11.09. gibt
es die eigene Adresse crew. mit einem offenen Modi-Knopf an der Wand --
es waere gut moeglich gewesen, dass die Pruefung etwas bewacht, das es
nicht mehr gibt. Sie gilt: pruef-modi-verborgen setzt mit 80 Pruefungen
durch, dass ein Manager keinen Modi sieht.

UND DABEI KAM ETWAS SCHLIMMERES HERAUS. Auf der echten Seite gemessen:

  /workspace/start.html                 302   (Anmeldung noetig)
  /workspace/assets/js/talente.js       200   25 KB Quelltext
  /workspace/assets/js/chat.js          200   85 KB
  /workspace/assets/css/entwicklung.css 200   34 KB

Jede Datei unter workspace/ ist OHNE JEDE ANMELDUNG aus dem offenen
Netz abrufbar. Der verborgene Zugang stand also nicht in einer Datei,
die "jeder herunterlaedt, der angemeldet ist" -- sondern in einer, die
man einfach abrufen kann.

Die Rollentabelle in workspace.js sagt seit dem 11.09. genau das
Richtige dazu: "Der Name steht hier und NICHT in einer Datei, die jeder
herunterlaedt -- ausgeliefert wird er nur an den, der ihn selbst
traegt, und an die, die ihn sehen duerfen." Danach ist jetzt gebaut,
ueberall:

  - talente.js hatte `rolle: 'modi'` fest im Text, dazu den Namen im
    Codekasten und im Rechtevergleich. Die Seite FRAGT ihn jetzt ab;
    /talente/lage liefert ihn, und die Route geht nur an DogFather und
    die rechte Hand.
  - Der Regeltext des Treffs nennt den Namen weiterhin -- Filipe hat am
    11.09. ausdruecklich gewaehlt, dass die Community ihn sieht. Er
    kommt jetzt als DATEN mit der Antwort, nicht als fester Text.
  - Das Feld hiess selbst `modi_frist_tage`. Das Muster \bmodi\b trifft
    das nicht, weil danach ein Unterstrich folgt -- ein Leck durch eine
    Luecke im Muster, nicht durch eine Entscheidung. Heisst jetzt
    `pause_frist_tage`.
  - Die uebrigen elf Fundstellen waren Kommentare. Umformuliert, ohne
    dass sie weniger erklaeren.

Ergebnis: 17 -> 0. Ohne Ausnahmeliste, ohne die Pruefung zu entschaerfen.

ZWEI LOECHER, DIE ICH DABEI SELBST GERISSEN HAETTE:

  darfAnlegen wurde aus `lage` gerechnet -- aber ichHolen() laeuft VOR
  laden(), also war `lage` noch null. Der Wert waere dauerhaft falsch
  gewesen und der Knopf nie erschienen. Jetzt ist es eine Frage statt
  eines Wertes, beantwortet beim Zeichnen. Gefunden beim Nachsehen der
  Reihenfolge, nicht im Testlauf -- deshalb steht die Frage ab jetzt in
  pruef-uebergang.

  Und im HTML des Treffs steht als Ersatzfassung "jemandem aus dem
  Team". Kommt der Name nicht an, bleibt sie stehen, die Seite sieht
  vollkommen richtig aus, und niemand merkt es. Ein Platzhalter "…"
  waere aufgefallen; eine richtige Ersatzfassung faellt nicht auf.
  pruef-neue-seiten prueft jetzt, dass wirklich der Name dasteht.

  pruef-modi-wortleck        0 Fehler (vorher 17 Fundstellen)
  pruef-uebergang 65 (vorher 61), pruef-neue-seiten 97 (vorher 93)
  pruef-modi-verborgen 80, pruef-nachwuchs 123, pruef-treff 66,
  pruef-treff-werkzeuge 70, pruef-css-klassen -- alle 0 Fehler

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

612 lines
25 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.
/* =====================================================================
Talente — der Trichter (11.09.2026)
Fünf Stufen, und jede hat GENAU EINEN nächsten Schritt. Das ist der
ganze Unterschied zu einer Liste: Eine Liste wächst und wird nie
wieder angesehen; ein Trichter fragt bei jeder Karte „und jetzt?".
DREI DINGE, DIE DIESE SEITE ABSICHTLICH TUT:
1. SIE ZEIGT DIE STANDZEIT. „Seit 41 Tagen beobachtet, nie
angesprochen" ist eine Auskunft über UNS, nicht über den
Kandidaten. Ohne sie wird der Trichter wieder zur Liste.
2. SIE TRENNT WARNZEICHEN VON MERKMALEN. Bei Moderatoren sagt das
Fehlverhalten mehr voraus als das Können — ein Warnzeichen neben
zwanzig Haken ginge unter.
3. SIE RECHNET NICHT. Keine Punktzahl, keine Eignung in Prozent.
Zwanzig Haken und ein Warnzeichen ergeben keine Zahl, sondern ein
Gespräch.
===================================================================== */
(() => {
const $ = (id) => document.getElementById(id);
const el = (t, k, text) => {
const n = document.createElement(t);
if (k) n.className = k;
if (text !== undefined) n.textContent = text;
return n;
};
const melde = (t) => { const f = $('fehler'); if (f) { f.textContent = t; f.hidden = !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('unbekannt'); }
return a;
}
/* TAGESDATUM IN ORTSZEIT. Nicht toISOString().slice(0,10) -- das
rechnet in UTC und liefert in Deutschland zwischen Mitternacht und
zwei Uhr morgens den Vortag. Ein Termin, der beim Eintragen einen
Tag zurueckspringt, ist genau die Sorte Fehler, die man erst
bemerkt, wenn jemand umsonst gewartet hat. */
const tag = (d) => {
const z = (n) => String(n).padStart(2, '0');
return `${d.getFullYear()}-${z(d.getMonth() + 1)}-${z(d.getDate())}`;
};
const heute = () => tag(new Date());
const inTagen = (n) => { const d = new Date(); d.setDate(d.getDate() + n); return tag(d); };
const tageBis = (s) => {
if (!s) return null;
const a = new Date(heute() + 'T12:00:00');
const b = new Date(s + 'T12:00:00');
return Number.isFinite(b.getTime()) ? Math.round((b - a) / 86400000) : null;
};
let lage = null;
/* WAS ICH ANLEGEN DARF -- die rohe Liste vom Server.
Ausgewertet wird sie erst beim Zeichnen: Welcher Zugang hier
entsteht, sagt `lage`, und die wird NACH dieser Liste geholt.
Ein Wert, der zu frueh gerechnet wird, ist dauerhaft falsch --
der Knopf waere nie erschienen. */
let darfListe = null;
const darfAnlegen = () => Array.isArray(darfListe)
&& !!lage?.zugang?.rolle && darfListe.includes(lage.zugang.rolle);
const offen = new Set(); // welche Karten aufgeklappt sind
/* ---- Der Trichter oben ---------------------------------------------- */
function trichter() {
const ziel = $('trichter');
ziel.textContent = '';
ziel.setAttribute('aria-busy', 'false');
for (const s of lage.trichter) {
const k = el('div', 't-stufe');
k.dataset.stufe = s.schluessel;
k.append(el('span', 't-stufe__zahl', String(s.anzahl)));
k.append(el('span', 't-stufe__name', s.name));
if (s.liegt) {
/* LIEGENGEBLIEBEN IST UNSERE ZAHL, nicht die des Kandidaten.
Sie steht deshalb am Trichter und nicht als Mahnung. */
const z = el('span', 't-stufe__liegt', `${s.liegt} liegt`);
z.title = 'So lange, dass es eine Auskunft über uns ist – nicht über sie.';
k.append(z);
}
ziel.append(k);
}
}
/* ---- Eine Karte ------------------------------------------------------ */
function karte(k) {
const a = el('article', 't-karte');
a.dataset.stufe = k.stufe;
if (k.liegt) a.dataset.liegt = 'ja';
const kopf = el('div', 't-karte__kopf');
kopf.append(el('h2', 't-karte__titel', k.titel));
kopf.append(el('span', 't-karte__stufe', k.stufe_name));
kopf.append(el('span', 't-karte__zeit',
k.tage === null ? '' : `seit ${k.tage} ${k.tage === 1 ? 'Tag' : 'Tagen'}`));
if (k.warnzeichen.length) {
const w = el('span', 't-karte__warn', `${k.warnzeichen.length} × hinsehen`);
w.title = 'Kein Ausschluss – ein Grund, länger hinzusehen.';
kopf.append(w);
}
a.append(kopf);
if (k.einsatz) a.append(el('p', 't-karte__wo', k.einsatz));
if (k.text) a.append(el('p', 't-karte__text', k.text));
/* DER EINE NÄCHSTE SCHRITT. Nicht drei Möglichkeiten — einer.
Wer eine Karte öffnet, soll in zwei Sekunden wissen, was dran
ist, und nicht erst eine Entscheidung treffen müssen. */
const schritt = el('div', 't-schritt');
schritt.append(el('p', 't-schritt__satz', k.schritt));
/* DER LETZTE SCHRITT IST KEIN SCHRITT, SONDERN EINE ENTSCHEIDUNG.
Aus einem Namen wird ein Mensch mit einem Zugang. Deshalb haengt
genau daran das Anlegen selbst -- und nicht ein Satz, den man
danach von Hand nachbaut. Bis heute stand hier "Zugang in
Personen & Zugaenge anlegen", und wer das vergass,
hatte eine Karte auf "Im Team" und keinen Menschen darin. */
const zugangNoetig = k.naechste === 'imteam';
if (k.knopf && k.naechste && (!zugangNoetig || darfAnlegen())) {
const b = el('button', 'schritt t-schritt__knopf',
zugangNoetig ? 'Ins Team holen · Zugang anlegen' : k.knopf);
b.type = 'button';
b.addEventListener('click', async () => {
b.disabled = true;
/* ZUERST DIE ZWEI FELDER, DANN DIE STUFE. Andersherum stünde
die Karte auf „Probe", während der Buddy noch offen ist —
und genau dieser Zustand war bisher der Normalfall. */
let uebergang = {};
if (k.naechste === 'probe') {
const antwort = await uebergangFragen(k, a, {
titel: 'Probe: zwei Dinge, dann läuft es',
satz: 'Wer ihn einarbeitet und wann die erste Schicht ist. Beides jetzt, '
+ 'nicht „melden wir uns“ — zwischen Zusage und erster Schicht springen '
+ 'die meisten ab.',
knopf: 'Auf Probe nehmen',
});
if (!antwort) { b.disabled = false; return; }
uebergang = antwort;
}
let zugang = null;
if (zugangNoetig) {
zugang = await zugangAnlegen(k);
if (!zugang) { b.disabled = false; return; }
}
const r = await hole(`/workspace/api/talente/${k.id}/stufe`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ stufe: k.naechste, ...uebergang }),
});
b.disabled = false;
if (!r.ok) {
/* ZWEI DINGE SIND PASSIERT, EINES DAVON HAT GEKLAPPT. Ein
blosses "ging nicht" waere hier die gefaehrlichste
Auskunft von allen -- der Zugang steht ja, und niemand
wuesste es. */
melde(zugang
? `Der Zugang für ${zugang.name} steht – der Code steht oben. Nur die Karte `
+ 'blieb auf „Probe“. Bitte den Schritt gleich noch einmal setzen.'
: absage(await r.json().catch(() => ({}))));
return;
}
offen.add(k.id);
await laden();
});
schritt.append(b);
} else if (zugangNoetig && k.knopf) {
/* KEIN KNOPF INS LEERE. Die rechte Hand sieht den Trichter
vollstaendig, aber Zugaenge vergibt nur DogFather. Ein Knopf,
der ihr jedes Mal "darfst du nicht" antwortet, waere schlechter
als gar keiner -- er verspricht etwas. */
schritt.append(el('p', 't-schritt__liegt',
'Den Zugang legt DogFather an – sag ihm Bescheid. Die Karte bleibt so '
+ 'lange auf „Probe“.'));
}
/* DER VORLAGENTEXT -- genau auf der Stufe, auf der er gebraucht
wird. Eine Schaltfläche „Text anzeigen" auf jeder Stufe wäre auf
vier von fünf davon eine Ablenkung. */
if (k.stufe === 'beobachtet' && k.ansprechtext) {
const b = el('button', 'schritt t-schritt__text', 'Vorlagentext');
b.type = 'button';
b.addEventListener('click', () => textZeigen(k));
schritt.append(b);
}
if (k.liegt) {
schritt.append(el('p', 't-schritt__liegt',
'Das liegt länger, als es sollte. Entweder weiter – oder bewusst beenden.'));
}
a.append(schritt);
/* ---- Buddy und erste Schicht ---------------------------------------
Sie stehen direkt unter dem nächsten Schritt und über allem
anderen: Während einer Probe sind sie die Karte. */
if (k.uebergang) {
const u = el('div', 't-uebergang');
if (k.uebergang.buddy_weg) u.dataset.weg = 'ja';
const zeile = el('p', 't-uebergang__zeile');
zeile.append(el('span', 't-uebergang__was', 'Arbeitet ein'));
zeile.append(el('span', 't-uebergang__wer',
k.uebergang.buddy_name || 'niemand mehr – bitte neu bestimmen'));
u.append(zeile);
if (k.uebergang.satz) u.append(el('p', 't-uebergang__satz', k.uebergang.satz));
/* WAS JEMAND AM ERSTEN TAG BEKOMMT. Drei Dinge, keine Haken.
Sie stehen hier, damit der Buddy sie liest — eine Liste zum
Abhaken wäre die zweite neben der im Wissensbereich. */
if (k.stufe === 'probe' && (lage.erster_tag || []).length) {
const liste = el('ul', 't-uebergang__tag');
for (const p of lage.erster_tag) {
const li = el('li', 't-uebergang__punkt');
li.append(el('strong', null, p.titel));
li.append(document.createTextNode(' — ' + p.text));
liste.append(li);
}
u.append(el('p', 't-uebergang__was', 'Am ersten Tag braucht er drei Dinge'));
u.append(liste);
}
if (k.stufe === 'probe') {
const aendern = el('button', 'schritt t-uebergang__aendern', 'Buddy oder Termin ändern');
aendern.type = 'button';
aendern.addEventListener('click', async () => {
const antwort = await uebergangFragen(k, a, {
titel: 'Buddy oder Termin ändern',
satz: 'Ein Buddy hört auf, ein Termin verschiebt sich. Die Standzeit der '
+ 'Karte bleibt dabei stehen — sie zählt, seit wann die Probe läuft.',
knopf: 'Übernehmen',
buddy: k.uebergang.buddy_id,
datum: k.uebergang.erste_schicht,
});
if (!antwort) return;
const r = await hole(`/workspace/api/talente/${k.id}/uebergang`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(antwort),
});
if (!r.ok) { melde(absage(await r.json().catch(() => ({})))); return; }
await laden();
});
u.append(aendern);
}
a.append(u);
}
/* ---- Merkmale, aufklappbar ------------------------------------------
Zugeklappt steht dort eine Zahl, aufgeklappt die 21 Merkmale.
Immer offen wären es bei zehn Kandidaten 270 Kästchen auf einer
Seite — und dann klickt niemand mehr eines an. */
const auf = offen.has(k.id);
const schalter = el('button', 't-karte__auf',
auf ? 'Merkmale zuklappen'
: `Merkmale ansehen (${k.merkmale.length} gesetzt${k.warnzeichen.length
? `, ${k.warnzeichen.length} Warnzeichen` : ''})`);
schalter.type = 'button';
schalter.setAttribute('aria-expanded', String(auf));
schalter.addEventListener('click', () => {
if (offen.has(k.id)) offen.delete(k.id); else offen.add(k.id);
zeichne();
});
a.append(schalter);
if (auf) {
for (const g of lage.gruppen) {
const block = el('section', 't-gruppe');
block.append(el('h3', 't-gruppe__titel', g.name));
block.append(el('p', 't-gruppe__satz', g.text));
const reihe = el('div', 't-merkmale');
for (const m of g.merkmale) reihe.append(merkmal(k, m, false));
block.append(reihe);
a.append(block);
}
const warn = el('section', 't-gruppe t-gruppe--warn');
warn.append(el('h3', 't-gruppe__titel', 'Hinsehen'));
warn.append(el('p', 't-gruppe__satz',
'Keines davon ist ein Ausschluss. Bei Moderatoren sagt das Verhalten, '
+ 'das stört, mehr voraus als das, was jemand kann.'));
const reihe = el('div', 't-merkmale');
for (const m of lage.warnzeichen) reihe.append(merkmal(k, m, true));
warn.append(reihe);
a.append(warn);
}
return a;
}
/* ---- Aus der Probe wird ein Zugang ---------------------------------
DERSELBE WEG WIE IN "PERSONEN & ZUGAENGE", nicht ein zweiter.
/workspace/api/verwaltung/personen ist die einzige Stelle im ganzen
Haus, an der ein Zugang entsteht -- mit ihrer Rechtepruefung, ihrer
Rollenpruefung, ihrem Protokolleintrag und ihrem genau einmal
angezeigten Code. Ein eigener Weg von hier waere eine zweite Tuer
zu genau dem, was am wenigsten zwei Tueren haben darf.
NICHT ueber hole(): Diese Adresse antwortet Nicht-DogFather mit
404, und hole() versteht ein 404 als "Seite gibt es nicht" und
schickt zur Startseite. Ein Rechtefehler wuerde die rechte Hand
also aus dem Trichter werfen. Hier wird deshalb selbst gelesen.
@returns {object|null} die angelegte Person, oder null bei Abbruch. */
async function zugangAnlegen(k) {
if (!darfAnlegen()) { melde('Zugänge legt DogFather an.'); return null; }
const vorschlag = String(k.titel || '').split(/[\s,(–-]+/)[0] || '';
const name = prompt(
'Wie soll die Person im Workspace heißen?\n\n'
+ 'Das ist der Name, den euer Team sieht. Gleich danach bekommst du '
+ 'einmalig ihren Zugangscode – danach nie wieder.',
vorschlag);
if (name === null) return null; // abgebrochen
if (String(name).trim().length < 2) { melde('Der Name ist zu kurz.'); return null; }
let a;
try {
a = await fetch('/workspace/api/verwaltung/personen', {
credentials: 'same-origin',
method: 'POST',
headers: { 'Content-Type': 'application/json' },
/* WELCHE ROLLE, SAGT DER SERVER (16.09.2026). Hier stand ein
fester Text -- und diese Datei liefert der Server an jeden
aus, der die Adresse kennt. Nachgemessen: ohne Anmeldung
200, 25 KB Quelltext. */
body: JSON.stringify({ name: String(name).trim(), rolle: lage.zugang.rolle }),
});
} catch { melde('Keine Verbindung – der Zugang wurde nicht angelegt.'); return null; }
if (a.status === 404 || a.status === 403) {
melde('Zugänge legt DogFather an – sag ihm Bescheid.');
return null;
}
if (!a.ok) {
const grund = (await a.json().catch(() => ({}))).fehler;
melde(grund || 'Der Zugang ließ sich nicht anlegen.');
return null;
}
const neu = await a.json();
codeZeigen(neu);
return neu;
}
/* Der Code steht in einem Kasten, den man SCHLIESSEN muss -- nicht in
einer Meldung, die von selbst verschwindet. Er ist das Einzige auf
dieser Seite, das es nur ein einziges Mal gibt. */
function codeZeigen(neu) {
$('code-name').textContent = `${neu.name} · ${lage.zugang.name}`;
$('code-wert').textContent = neu.code;
$('code-kasten').hidden = false;
$('code-kasten').scrollIntoView({ behavior: 'smooth', block: 'center' });
$('code-kopieren').focus();
}
$('code-kopieren')?.addEventListener('click', async () => {
const code = $('code-wert').textContent;
try { await navigator.clipboard.writeText(code); melde('Code kopiert.'); }
catch {
/* Ohne Zwischenablage-Recht: markieren, dann geht Strg+C. Der
Code steht ja da -- "ging nicht" waere hier unbrauchbar. */
getSelection()?.removeAllRanges();
const r = document.createRange();
r.selectNodeContents($('code-wert'));
getSelection()?.addRange(r);
melde('Markiert – mit Strg+C kopieren.');
}
});
$('code-schliessen')?.addEventListener('click', () => {
$('code-kasten').hidden = true;
$('code-wert').textContent = ''; // auch aus der Seite raus
melde('');
});
/* DER TEXT WIRD GEZEIGT, NICHT VERSCHICKT.
Eine Nachricht, die ein Programm abschickt, liest sich wie eine
Nachricht, die ein Programm abgeschickt hat — und genau das soll
dieser Text nicht sein. Er steht da zum Lesen, Ändern und
Kopieren, in einem Feld, das man bearbeiten kann. */
function textZeigen(k) {
const alt = document.getElementById('t-textfeld');
if (alt) alt.closest('.t-text')?.remove();
const kasten = el('div', 't-text');
kasten.append(el('h3', 't-text__titel', `Vorschlag für ${k.titel}`));
kasten.append(el('p', 't-text__satz',
'Gebaut aus den Merkmalen, die du angeklickt hast. Ändere ihn ruhig – '
+ 'er soll nach dir klingen, nicht nach einer Vorlage. Abgeschickt wird '
+ 'hier nichts.'));
const feld = el('textarea', 't-text__feld');
feld.id = 't-textfeld';
feld.rows = 14;
feld.value = k.ansprechtext;
kasten.append(feld);
const reihe = el('div', 't-text__knoepfe');
const kopieren = el('button', 'schritt', 'Kopieren');
kopieren.type = 'button';
kopieren.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(feld.value);
melde('Kopiert. Jetzt einfügen, wo du ihn ansprichst.');
} catch {
/* Ohne Zwischenablage-Recht: markieren, dann geht Strg+C.
Eine Fehlermeldung „ging nicht" wäre hier unbrauchbar — der
Text ist ja da. */
feld.select();
melde('Markiert – mit Strg+C kopieren.');
}
});
reihe.append(kopieren);
const zu = el('button', 'schritt', 'Schließen');
zu.type = 'button';
zu.addEventListener('click', () => kasten.remove());
reihe.append(zu);
kasten.append(reihe);
const karte = [...document.querySelectorAll('.t-karte')]
.find((x) => x.querySelector('.t-karte__titel')?.textContent === k.titel);
(karte || $('liste')).append(kasten);
feld.focus();
}
/* =====================================================================
DER ÜBERGANG — ein Name und ein Datum (15.09.2026)
Filipe: „ich will die noch viel besser […] so einfach wie möglich
für jeden."
Die Auswahl war der gut gebaute Teil dieser Seite. Verloren gehen
Leute dahinter: zwischen dem „ja" und der ersten echten Schicht.
Wer zusagt und danach zwei Wochen nichts hört, hat innerlich schon
abgesagt — und auf der Karte steht weiter „angesprochen".
KEIN prompt() FÜR EIN DATUM. Der Zugang nebenan benutzt eins für
einen Namen, und dort geht das. Ein Datum, das jemand in ein
Textfeld tippt, kommt als „nächsten Samstag" zurück. Hier steht
deshalb eine Auswahl mit Namen und ein echter Datumsknopf — auf
dem Handy der Kalender des Geräts.
===================================================================== */
function uebergangFragen(k, karteEl, art) {
return new Promise((fertig) => {
document.querySelector('.t-uebergang--frage')?.remove();
const kasten = el('form', 't-uebergang t-uebergang--frage');
kasten.append(el('h3', 't-uebergang__titel', art.titel));
kasten.append(el('p', 't-uebergang__satz', art.satz));
const w1 = el('label', 't-uebergang__feld');
w1.append(el('span', 't-uebergang__was', 'Wer arbeitet ein'));
const wahl = el('select', 't-uebergang__wahl');
const leer = el('option', null, 'bitte wählen');
leer.value = '';
wahl.append(leer);
for (const p of (lage.buddys || [])) {
const o = el('option', null, p.name);
o.value = String(p.id);
if (art.buddy && art.buddy === p.id) o.selected = true;
wahl.append(o);
}
w1.append(wahl);
kasten.append(w1);
const w2 = el('label', 't-uebergang__feld');
w2.append(el('span', 't-uebergang__was', 'Erste Schicht'));
const datum = el('input', 't-uebergang__wahl');
datum.type = 'date';
datum.min = heute();
/* DREI TAGE ALS VORSCHLAG, nicht heute und nicht leer. Heute wäre
eine Behauptung, leer wäre eine Aufgabe. Drei Tage sind nah
genug, dass es nicht verläuft, und weit genug, dass man sich
abstimmen kann. */
datum.value = art.datum || inTagen(3);
w2.append(datum);
kasten.append(w2);
const hinweis = el('p', 't-uebergang__hinweis');
kasten.append(hinweis);
const nachsehen = () => {
const bis = tageBis(datum.value);
const weit = lage.schicht_weit_tage || 14;
hinweis.textContent = bis !== null && bis > weit
? `Das sind ${bis} Tage hin. Bis dahin hört man nichts voneinander – `
+ 'wenn es nicht anders geht, meldet euch zwischendurch einmal.'
: '';
hinweis.hidden = !hinweis.textContent;
};
datum.addEventListener('change', nachsehen);
nachsehen();
const reihe = el('div', 't-uebergang__knoepfe');
const ja = el('button', 'schritt t-uebergang__ok', art.knopf);
ja.type = 'submit';
reihe.append(ja);
const ab = el('button', 'schritt', 'Abbrechen');
ab.type = 'button';
ab.addEventListener('click', () => { kasten.remove(); fertig(null); });
reihe.append(ab);
kasten.append(reihe);
kasten.addEventListener('submit', (e) => {
e.preventDefault();
if (!wahl.value) { melde('Ohne einen Namen geht die Probe nicht los.'); wahl.focus(); return; }
if (!datum.value) { melde('Und ohne Datum auch nicht.'); datum.focus(); return; }
melde('');
kasten.remove();
fertig({ buddy_id: Number(wahl.value), erste_schicht: datum.value });
});
karteEl.append(kasten);
wahl.focus();
});
}
/* Was der Server absagt, in einem Satz, der sagt WAS fehlt.
„Ging nicht" wäre hier die unbrauchbarste Auskunft überhaupt — die
beiden Felder standen ja gerade noch auf dem Bildschirm. */
const UEBERGANG_ABSAGE = {
buddy: 'Ohne einen Namen geht die Probe nicht los.',
buddy_unbekannt: 'Diese Person kann gerade niemanden einarbeiten.',
erste_schicht: 'Es fehlt das Datum der ersten Schicht.',
datum_unlesbar: 'Das Datum konnte ich nicht lesen.',
datum_zu_alt: 'Das Datum liegt zu weit zurück.',
datum_zu_weit: 'Das Datum liegt zu weit in der Zukunft – das ist kein Termin mehr.',
};
const absage = (antwort) => UEBERGANG_ABSAGE[(antwort?.fehlt || [])[0]] || 'Ging nicht.';
function merkmal(k, m, istWarnung) {
const gesetzt = (istWarnung ? k.warnzeichen : k.merkmale).includes(m.schluessel);
const b = el('button', 't-merkmal');
b.type = 'button';
if (istWarnung) b.dataset.warnung = 'ja';
b.setAttribute('aria-pressed', String(gesetzt));
b.title = m.text;
b.append(el('span', 't-merkmal__haken', gesetzt ? '✓' : '+'));
b.append(el('span', 't-merkmal__titel', m.titel));
b.addEventListener('click', async () => {
b.disabled = true;
const r = await hole(`/workspace/api/talente/${k.id}/merkmal/${m.schluessel}`,
{ method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: '{}' });
b.disabled = false;
if (!r.ok) { melde('Ging nicht.'); return; }
await laden();
});
return b;
}
/* ---- Zeichnen --------------------------------------------------------- */
function zeichne() {
trichter();
const ziel = $('liste');
ziel.textContent = '';
ziel.setAttribute('aria-busy', 'false');
if (!lage.kandidaten.length) {
const leer = el('div', 't-leer');
leer.append(el('p', null,
'Noch niemand auf der Liste. Ein Talent entsteht als Eintrag im Brett '
+ '„Talente" – dort schreibst du auf, wem du etwas zutraust, und hier '
+ 'wird daraus ein Weg.'));
const a = el('a', 'schritt', 'Zum Brett „Talente"');
a.href = 'bereich.html?b=talente';
leer.append(a);
ziel.append(leer);
return;
}
/* LIEGENGEBLIEBENE ZUERST. Die Reihenfolge ist die eigentliche
Arbeitsanweisung dieser Seite: Was liegt, ist das, was dran ist. */
const sortiert = [...lage.kandidaten].sort((a, b) =>
(b.liegt ? 1 : 0) - (a.liegt ? 1 : 0) || (b.tage || 0) - (a.tage || 0));
for (const k of sortiert) ziel.append(karte(k));
const fuss = el('p', 't-fuss');
fuss.append('Ein neues Talent legst du im Brett an – ');
const a = el('a', null, 'Talente-Brett öffnen');
a.href = 'bereich.html?b=talente';
fuss.append(a);
fuss.append('. Von dort landet es hier automatisch auf „Aufgefallen".');
ziel.append(fuss);
}
async function laden() {
const a = await hole('/workspace/api/talente/lage');
if (!a.ok) { melde('Ging nicht.'); return; }
lage = await a.json();
zeichne();
}
/* WER ZUGAENGE ANLEGEN DARF, SAGT DER SERVER -- dieselbe Auskunft,
aus der auch "Personen & Zugaenge" ihre Rollenknoepfe baut. Eine
eigene Rollenabfrage hier waere eine zweite Meinung darueber, und
die zweite ist die, die irgendwann mehr durchlaesst.
VOR dem ersten Zeichnen, nicht daneben: Sonst steht der Knopf
einen Wimpernschlag lang da und verschwindet wieder. */
async function ichHolen() {
try {
const a = await hole('/workspace/api/ich');
if (!a.ok) return;
const ich = await a.json();
window.werZeigen?.(ich);
darfListe = ich?.darf_anlegen;
} catch { /* umgeleitet */ }
}
(async () => {
try { await ichHolen(); await laden(); } catch { /* umgeleitet */ }
})();
})();