Vier Bildschirmfotos, vier Auftraege. Vor dem Bauen gemessen
(server/mess-sichtbarkeit.mjs): alle Bereiche, alle vier Rollen.
ZWEI FEHLER, DIE NIEMAND GEMELDET HAETTE
Beide sahen auf DogFathers eigenem Bildschirm vollkommen richtig aus --
und das ist das Tueckische: Ein leeres Brett sieht nicht nach Fehler
aus, sondern nach "da ist eben noch nichts".
1. DIE FREIGABE. "Was ansteht" und "Highlights" zeigen der Community
nur, was freigegeben ist. Ein aus dem Vorschlagskasten uebernommener
Eintrag hatte keine Freigabe. Gemessen:
Brett DogFather rechte Hand Modi Community
ansteht 4 4 4 0
highlight 3 3 3 0
Filipe konnte fuellen, so viel er wollte -- fuer die Community
blieben genau die zwei Bretter leer, auf die es ankommt. Wer aus dem
Team uebernimmt, gibt jetzt zugleich frei: Die Freigabe fragt "hat
das Team diesen Text gesehen?", und die Antwort ist beim Uebernehmen
ja. Eine zweite Entscheidung daneben waere keine Sicherheit, sondern
ein Klick.
2. TEAM_DOGI_ROLLEN IST {hand, modi} -- OHNE "admin". Die
Sichtbarkeitsregel fuer Team Dogi lautete damit woertlich "Eintraege
von hand oder modi". Folge: ALLES, was DogFather in Live, Technik,
Community, Ideen, Angebote oder Rueckmeldung schreibt, war fuer sein
eigenes Team unsichtbar. Das ist nicht erst seit dem Startkatalog so
-- der hat es nur sichtbar gemacht, weil vorher ueberall Null stand
und Null gleich Null aussieht, egal aus welchem Grund.
Die Liste wird genau dort erweitert, wo es um Sichtbarkeit geht --
NICHT in TEAM_DOGI_ROLLEN selbst: Diese Menge entscheidet auch,
welches Haus jemand bewohnt. Ein Eintrag dort haette DogFather
stillschweigend zur Crew umgezogen.
113 FERTIGE INHALTE IN DEN LEEREN KATEGORIEN
Gemessen waren elf Bereiche ausserhalb des Treffs komplett leer -- fuer
JEDE Rolle. Der Startkatalog kannte nur die sieben Treff-Bretter; die
Oberflaeche war die ganze Zeit bereit, es fehlten nur die Texte.
workspace-arbeit-start.js fuellt neun davon: Live, Content, Technik,
Community, Schutz, Ideen, Rueckmeldung, Angebote, Agentur.
"talente" und "entwicklung" bekommen ABSICHTLICH nichts: Dort waere ein
"fertiger Inhalt" ein erfundener Mensch -- ein Testdatensatz in einem
System, das echte Menschen bewertet. Die Talente-Seite bekommt ihre
Hilfe anders (siehe unten).
DER ENTWICKLUNGSKATALOG: 28 -> 68 PUNKTE, VIERTE STUFE
Filipe: "wieso ist da immer noch nicht perfektionniert wie bei den
anderen mit fortgeschritten und so, und VIIIIIEEEELLLLLL mehr aufgaben."
* Neue Stufe "Fortgeschritten" zwischen "nach ein paar Monaten" und
"wofuer man jemanden fragt" -- genau die Mitte fehlte.
* Stufenleiter als FILTER ueber den Kategorien, wie die Checkliste es
seit dem 10.09. vormacht. Aus r.erwartung gebaut, nicht aufgezaehlt.
* Zwei neue Bloecke, die Filipe ausdruecklich wollte: "Clips, Schnitt
und Kommentare" und "Waehrend der Stream laeuft" -- seine
Wunschkategorien vom 17.09. ("videos schneide, kommentieren,
markierungen").
* Keine Note, keine Punktzahl, keine Rangliste. Unveraendert.
DAS BEFINDEN: 6 -> 12 FRAGEN, UND SIE MELDET SICH VON SELBST
Den 14-Tage-Rhythmus gab es schon -- er wirkte aber nur, WENN jemand die
Seite aufmachte. Wer sie vergisst, wurde nie wieder gefragt: Das war
eine Anzeige, keine Anfrage. Jetzt kommt sie ueber dasselbe Push-System
wie faellige Aufgaben, hoechstens einmal je sieben Tage, und sie fragt
rhythmus() aus workspace-befinden.js -- dieselbe Funktion wie die Seite,
keine zweite Formel daneben.
Sechs neue Fragen zu Erholung, Sinn, Klarheit, Anfeindung, Entwicklung
und dem Blick nach vorn, jede mit ihrer Einordnung.
Und an acht Stellen stand "sechs Fragen" ueber zwoelf Fragen -- wo ein
Skript den Text baut, wird jetzt gezaehlt; in den festen Seiten steht
gar keine Zahl mehr.
DIE TALENTE-SEITE, WENN NIEMAND DARAUF STEHT
Statt "Noch niemand auf der Liste" stehen dort jetzt die fuenf Gruppen
mit ihren 21 Anzeichen -- die gab es laengst, sie waren auf der leeren
Seite nur nie zu sehen. Dazu der Weg in beide Richtungen: woher ein
Talent kommt (der Treff) und wo es endet (die Entwicklung).
DIE PRUEFUNG (server/pruef-alle-sehen-es.mjs, 42 Pruefungen)
Sie prueft BEIDE Haelften von Filipes Satz. Nur zu zaehlen, ob alle
alles sehen, waere gruen, wenn man saemtliche Schranken entfernte --
Abschnitt 4 belegt, dass 19 Bereich-Rollen-Paare zu bleiben und die
Community an keinen der elf Arbeitsbereiche kommt.
ZWEI GEGENPROBEN, weil 42 von 42 im ersten Lauf kein Beweis ist:
a) "admin" aus der Team-Sicht entfernen -> 13 Pruefungen rot
b) Freigabe beim Uebernehmen abschalten -> 2 rot, genau ansteht
und highlight
Jede trifft ihr Ziel und nicht alles.
UND EINE LUECKE IN EINER ALTEN PRUEFUNG
pruef-deutsche-texte war gruen -- und hatte die 113 neuen Texte, die 40
neuen Entwicklungspunkte und die 6 neuen Fragen nie gesehen: Sie stehen
in Dateien, die sie nicht kannte. Jetzt 391 statt 174 geprueften
Texten. (Meine erste Untergrenze stand auf 400 und war sofort rot --
geraten statt gemessen.)
Stempel 202609171318.
Co-Authored-By: Claude Opus 5 <[email protected]>
653 lines
27 KiB
JavaScript
653 lines
27 KiB
JavaScript
/* =====================================================================
|
||
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) {
|
||
/* DER LEERE ZUSTAND ARBEITET (17.09.2026).
|
||
|
||
Vorher stand hier ein Satz, der sagte, dass nichts da ist,
|
||
und ein Knopf, der woanders hinführt. Das ist keine
|
||
Auskunft, sondern eine Absage -- und genau der Eindruck,
|
||
den Filipe im Bildschirmfoto beschrieben hat.
|
||
|
||
Jetzt steht da, WORAUF man achtet: die fünf Gruppen aus
|
||
lage.gruppen mit der Zahl ihrer Anzeichen -- 21 Stück, die
|
||
es längst gibt und die auf der leeren Seite nie jemand zu
|
||
sehen bekam. Abgeleitet, nicht abgeschrieben: Kommt eine
|
||
sechste Gruppe dazu, steht sie hier von selbst.
|
||
|
||
UND ER BLEIBT EHRLICH: Es werden keine erfundenen Namen in
|
||
den Trichter gesetzt, damit er voll aussieht. Ein Talent ist
|
||
ein Mensch, kein Inhalt. */
|
||
const leer = el('div', 't-leer');
|
||
leer.append(el('h3', 't-leer__titel', 'Wonach du suchst'));
|
||
leer.append(el('p', null,
|
||
'Noch niemand auf der Liste – das ist der Normalzustand am Anfang. '
|
||
+ 'Ein Talent fällt zuerst im Chat auf, nicht in einer Bewerbung. '
|
||
+ 'Das hier sind die fünf Dinge, die zählen:'));
|
||
const merkmale = el('ul', 't-leer__merkmale');
|
||
for (const g of lage.gruppen || []) {
|
||
const li = el('li', 't-leer__merkmal');
|
||
li.dataset.art = g.schluessel;
|
||
li.append(el('span', 't-leer__merkmal-name', g.name));
|
||
if (g.merkmale?.length) {
|
||
li.append(el('span', 't-leer__merkmal-zahl',
|
||
`${g.merkmale.length} Anzeichen`));
|
||
}
|
||
merkmale.append(li);
|
||
}
|
||
if (merkmale.children.length) leer.append(merkmale);
|
||
leer.append(el('p', 't-leer__weg',
|
||
'Wer dir auffällt, kommt als Eintrag ins Brett „Talente“. '
|
||
+ 'Von dort wird hier ein Weg daraus: aufgefallen, beobachtet, '
|
||
+ 'angesprochen, auf Probe – und am Ende im Team.'));
|
||
const knoepfe = el('div', 't-leer__knoepfe');
|
||
const a = el('a', 'schritt', 'Jemanden eintragen');
|
||
a.href = 'bereich.html?b=talente';
|
||
knoepfe.append(a);
|
||
/* DER WEG ZURÜCK ZUM TREFF. Dort fällt jemand auf -- das ist
|
||
der Anfang der Kette, und ohne ihn beginnt diese Seite im
|
||
Nichts. */
|
||
const t = el('a', 'schritt schritt--leise', 'Im Treff umsehen');
|
||
t.href = 'bereich.html?b=treff';
|
||
knoepfe.append(t);
|
||
leer.append(knoepfe);
|
||
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 */ }
|
||
})();
|
||
})();
|