Deutsch oeffnet mit „ und schliesst mit “. An 199 Stellen, die ein
Mensch liest, stand als Schlusszeichen ein GERADES " -- „Nächstes"
statt „Nächstes“. Auf sechzehn oeffentlichen Seiten, in vierzig
Dateien des Workspace und in zwoelf Servermodulen, die Texte
verschicken.
Auf dem Bildschirm sieht man den Unterschied sofort. Beim Schreiben
nicht: Das gerade " liegt auf der Tastatur, die anderen nicht.
WARUM EIN ERSTER ANLAUF ZURUECKGENOMMEN WURDE
Ein gerades " ist an vielen Stellen SYNTAX und kein Schriftzeichen --
Grenze einer Zeichenkette, Grenze eines HTML-Attributs, Zeichen in
einem regulaeren Ausdruck. Wer stumpf ersetzt, macht aus
„<a href="https://… ein kaputtes Attribut
/^["'„»\s]+|["'“«.\s]+$/ einen kaputten Ausdruck
"… nichts „mal " + "eben …" eine kaputte Zeichenkette
DIE UNTERSCHEIDUNG LAEUFT AN MERKMALEN, NICHT AN EINER LISTE
< > = dazwischen -> HTML-Marke oder Attribut
endet auf Leerzeichen -> die Zeichenkette hoert hier auf, der
Satz geht in der naechsten Zeile weiter.
Ein deutsches Schlusszeichen steht NIE
hinter einem Leerzeichen.
Rueckstrich mittendrin -> regulaerer Ausdruck
${ ohne } -> mitten in einem Ausdruck
Eine Liste erlaubter Ausnahmen waere die naechste, die niemand
pflegt. Zwoelf Stellen bleiben dadurch stehen, alle zwoelf einzeln
angesehen und alle zwoelf zu Recht -- dort steht das richtige
Schlusszeichen ohnehin weiter unten im Satz.
Fuenf davon waren allerdings ECHTE Fehler HINTER dem Link
(`…>HasiDog</a>".`) -- die erste Regel hatte nur das Attribut
gesehen, nicht den Satz danach. Gezielt nachgezogen.
Ein maskiertes `\"` in workspace-vorlagen.js (28 Hooks) wird zu “ --
ohne Rueckstrich, denn “ begrenzt nichts.
KOMMENTARE BLEIBEN, WIE SIE SIND. Dort liest es niemand ausser mir;
eine Wache, die auch Kosmetik anmahnt, wird weggeklickt. Beim ersten
Messen fielen ausserdem acht Stellen aus buehne.html faelschlich an,
weil `/* */` in HTML (in <style> und <script>) nicht ausgeblendet
war -- jetzt schon.
DIE WACHE DAZU
pruef-struktur prueft es ab sofort mit derselben Regel: 322 Dateien
mit sichtbarem Text, 0 Funde, und die zwoelf bewussten Ausnahmen
werden GEZAEHLT und genannt (erlaubt: 12). Eine Ausnahme, die niemand
sieht, waechst -- und irgendwann steht der echte Fall darin.
GEPRUEFT
pruef-struktur 59 -> 68 Pruefungen, 0 Fehler
node --check auf allen geaenderten JS-Dateien
nachgemessen: 199 geaendert, 12 mit Grund stehen geblieben
gruen geblieben: bewerbung-aufgaben 163, nachwuchs 262,
reaktion 421, support 63, content 45, terminregel 35, treff 85
Und nachgesehen, ob eine Pruefung noch die alte Schreibweise
ERWARTET: 13 Fundstellen, alle dreizehn nur Text in ihrer eigenen
Ausgabe, keine einzige ein Vergleich mit dem Seitentext.
GEGENPROBE: In reaktion.html ein Schlusszeichen zurueckgedreht ->
„workspace/reaktion.html:546 „Nächstes"", mit Datei und Zeile. Und
die Erkennung einzeln gegen HTML-Attribut, fortgesetzte
Zeichenkette, regulaeren Ausdruck und eingesetzten Wert geprueft.
Stempel gesetzt: workspace 670 Verweise in 45 Dateien, oeffentlich
554 in 48 Seiten.
Co-Authored-By: Claude Opus 5 <[email protected]>
598 lines
24 KiB
JavaScript
598 lines
24 KiB
JavaScript
/* ===================================================================
|
||
Scout-CRM (Konzept, Seite 15).
|
||
|
||
Die Pipeline ist nach Stufen gruppiert, nicht als breites Board --
|
||
auf dem Handy waere ein Board mit sechs Spalten unbedienbar, und ein
|
||
Scout arbeitet ohnehin meistens unterwegs.
|
||
|
||
Jede Karte hat genau einen naheliegenden naechsten Schritt ("Weiter
|
||
zu ..."), weil die Pipeline eine Richtung hat. Alles andere steckt
|
||
im aufklappbaren Teil, damit die Liste ruhig bleibt.
|
||
=================================================================== */
|
||
(() => {
|
||
'use strict';
|
||
|
||
/* DogFather und Manager haben dieselben Rechte -- die Unterscheidung
|
||
macht nur die Personenverwaltung, und die prueft der Server. */
|
||
/* Die Leitungsliste steht in bereiche.js -- eine Antwort statt neun.
|
||
Sie stand hier frueher selbst, ohne 'spicy', und genau das war der
|
||
Fehler, den Filipe am 07.09.2026 gefunden hat. */
|
||
const LEITUNG = window.Bereiche.LEITUNG;
|
||
|
||
const $ = (id) => document.getElementById(id);
|
||
const WEITER = {
|
||
neu: 'angesprochen', angesprochen: 'gespraech',
|
||
gespraech: 'interessiert', interessiert: 'uebergeben',
|
||
};
|
||
|
||
/* WAS JEDE STUFE BEDEUTET -- an EINER Stelle.
|
||
|
||
Diese Saetze standen bis zum 11.09.2026 nur im leeren Zustand der
|
||
Seite, ausgeschrieben in einer eigenen Liste. Jetzt lesen beide
|
||
daraus: die Erklaerung fuer den leeren Zustand und der Kopf jeder
|
||
Stufe. Zwei Fassungen derselben Erklaerung waeren zwei
|
||
Gelegenheiten, dass eine von beiden veraltet -- und eine veraltete
|
||
Erklaerung ist schlimmer als keine. */
|
||
const STUFE_WAS = {
|
||
neu: 'Entdeckt. Noch nicht angeschrieben.',
|
||
angesprochen: 'Erste Nachricht ist raus. Jetzt läuft die Uhr.',
|
||
gespraech: 'Ihr seid im Austausch – Fragen, Bedingungen, Termine.',
|
||
interessiert: 'Grundsätzlich dabei. Jetzt zählt, dass es nicht liegen bleibt.',
|
||
uebergeben: 'Ein Manager übernimmt. Ab hier ist es Betreuung, kein Scouting mehr.',
|
||
abgelehnt: 'Passt nicht – festgehalten, damit niemand zweimal fragt.',
|
||
};
|
||
|
||
let ich = null;
|
||
let stufen = {};
|
||
/* Die Auswahllisten kommen vom Server (siehe /api/leads) -- die
|
||
Oberflaeche fuehrt keine eigenen. Leere Vorgabe, damit ein
|
||
Ausfall der Abfrage kein kaputtes Formular ergibt. */
|
||
let laenderListe = [], landName = {}, netzwerkListe = [], netzwerkName = {};
|
||
let heute = '';
|
||
let daten = [];
|
||
let offen = new Set();
|
||
|
||
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) => { $('fehler').textContent = t || ''; };
|
||
const datum = (iso) => (iso ? iso.slice(0, 10).split('-').reverse().join('.') : '');
|
||
|
||
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 && !pfad.includes('/leads/')) {
|
||
location.assign('/workspace/start.html'); throw new Error('kein Zugriff');
|
||
}
|
||
return a;
|
||
}
|
||
|
||
async function schicke(pfad, methode, körper) {
|
||
const a = await hole(pfad, {
|
||
method: methode,
|
||
headers: körper ? { 'Content-Type': 'application/json' } : undefined,
|
||
body: körper ? JSON.stringify(körper) : undefined,
|
||
});
|
||
if (!a.ok) {
|
||
melde(sagWas((await a.json().catch(() => ({}))).fehler, 'Das hat nicht geklappt.'));
|
||
return null;
|
||
}
|
||
melde('');
|
||
return a.json().catch(() => ({}));
|
||
}
|
||
|
||
/* ---------- Karte -------------------------------------------------------- */
|
||
|
||
function zeile(schild, wert) {
|
||
const p = el('p', 'kk__zeile');
|
||
p.append(el('span', 'kk__schild', schild), document.createTextNode(wert));
|
||
return p;
|
||
}
|
||
|
||
/* 12.400 -> "12,4k". Auf einer Karte zaehlt die Groessenordnung, nicht
|
||
die letzte Stelle -- und die genaue Zahl steht im Titel. */
|
||
function zahlKurz(n) {
|
||
if (!Number.isFinite(n)) return '';
|
||
if (n >= 1000000) return (n / 1000000).toFixed(n >= 10000000 ? 0 : 1).replace('.', ',') + 'M';
|
||
if (n >= 1000) return (n / 1000).toFixed(n >= 10000 ? 0 : 1).replace('.', ',') + 'k';
|
||
return String(n);
|
||
}
|
||
|
||
function karte(l) {
|
||
const k = el('article', 'kk');
|
||
k.dataset.prio = l.prioritaet;
|
||
const fällig = l.naechster_followup && l.naechster_followup <= heute
|
||
&& l.status !== 'uebergeben' && l.status !== 'abgelehnt';
|
||
if (fällig) k.dataset.faellig = 'ja';
|
||
|
||
/* Kopf */
|
||
const kopf = el('div', 'kk__kopf');
|
||
const links = el('div');
|
||
links.append(el('h3', 'kk__name', l.name));
|
||
const unter = [l.plattform, l.handle].filter(Boolean).join(' · ');
|
||
if (unter) links.append(el('p', 'kk__unter', unter));
|
||
kopf.append(links);
|
||
|
||
const marken = el('div', 'kk__marken');
|
||
/* SCHON IM NETZWERK? ZUERST UND IN ROT (11.09.2026).
|
||
|
||
Das ist die einzige Angabe, die die ganze weitere Arbeit
|
||
erledigt: Wer unter Vertrag steht, kann nicht uebernommen
|
||
werden. Sie steht deshalb VOR der Prioritaet -- eine hohe
|
||
Prioritaet bei jemandem, der gar nicht kann, ist eine
|
||
Verabredung mit sich selbst. */
|
||
if (l.netzwerk === 'ja') {
|
||
const n = el('span', 'marke-punkt', 'Schon im Netzwerk');
|
||
n.dataset.warn = 'ja';
|
||
n.title = 'Steht bei einer anderen Agentur unter Vertrag – eine Übernahme geht nicht.';
|
||
marken.append(n);
|
||
} else if (l.netzwerk === 'unbekannt') {
|
||
const n = el('span', 'marke-still', 'Netzwerk ungefragt');
|
||
n.title = 'Noch nicht geklärt, ob die Person schon bei einer Agentur ist.';
|
||
marken.append(n);
|
||
}
|
||
const prio = el('span', 'marke-punkt', l.prioritaet === 'hoch' ? 'Hoch'
|
||
: l.prioritaet === 'niedrig' ? 'Niedrig' : 'Mittel');
|
||
prio.dataset.prio = l.prioritaet;
|
||
marken.append(prio);
|
||
if (l.scout_name && LEITUNG.has(ich.rolle)) marken.append(el('span', 'marke-still', l.scout_name));
|
||
if (l.creator_name) marken.append(el('span', 'marke-fertig', 'Creator: ' + l.creator_name));
|
||
kopf.append(marken);
|
||
k.append(kopf);
|
||
|
||
/* Kurzinfo */
|
||
const info = el('div', 'kk__info');
|
||
/* DIE HARTEN ZAHLEN ZUERST (11.09.2026). Land, Reichweite und
|
||
LIVE-Zeiten beantworten "lohnt sich das ueberhaupt" -- und zwar
|
||
ohne dass man die Karte aufklappen muss. Fehlt eine Angabe,
|
||
steht sie nicht da; ein leeres Schild ist schlechter als keins. */
|
||
if (l.land) {
|
||
const s = el('span', 'kk__fakt', landName[l.land] || l.land);
|
||
s.dataset.art = 'land';
|
||
info.append(s);
|
||
}
|
||
if (l.follower) {
|
||
const f = el('span', 'kk__fakt', zahlKurz(l.follower) + ' Follower');
|
||
f.dataset.art = 'follower';
|
||
f.title = l.follower.toLocaleString('de-DE') + ' Follower beim Fund';
|
||
info.append(f);
|
||
}
|
||
if (l.live_zeiten) info.append(el('span', 'kk__still', 'LIVE: ' + l.live_zeiten));
|
||
if (l.naechster_followup) {
|
||
const f = el('span', 'kk__followup', 'Follow-up ' + datum(l.naechster_followup));
|
||
if (fällig) f.dataset.faellig = 'ja';
|
||
info.append(f);
|
||
}
|
||
if (l.letzte_nachricht) info.append(el('span', 'kk__still', 'zuletzt geschrieben ' + datum(l.letzte_nachricht)));
|
||
if (l.uebergeben_am) info.append(el('span', 'kk__still', 'übergeben ' + datum(l.uebergeben_am)));
|
||
if (info.childNodes.length) k.append(info);
|
||
|
||
/* Knöpfe: ein naheliegender Schritt, alles andere aufklappbar. */
|
||
const knöpfe = el('div', 'kk__knoepfe');
|
||
const ziel = WEITER[l.status];
|
||
if (ziel) {
|
||
const w = el('button', 'knopf knopf--klein knopf--weiter', 'Weiter zu „' + stufen[ziel] + '“');
|
||
w.type = 'button';
|
||
w.addEventListener('click', () => stufeSetzen(l, ziel));
|
||
knöpfe.append(w);
|
||
}
|
||
if (l.status === 'uebergeben' && LEITUNG.has(ich.rolle) && !l.creator_id) {
|
||
const o = el('button', 'knopf knopf--klein knopf--gold', 'Creator-Onboarding starten');
|
||
o.type = 'button';
|
||
o.addEventListener('click', () => onboarding(l, o));
|
||
knöpfe.append(o);
|
||
}
|
||
/* „BEARBEITEN" STATT „DETAILS" (11.09.2026).
|
||
|
||
Filipe: "weil man kan da nichts machen." Man konnte -- der ganze
|
||
Editor lag hinter diesem Knopf. Nur verspricht "Details" das
|
||
Lesen und nicht das Aendern, und er stand als stiller Textknopf
|
||
neben einem auffaelligen. Wer ihn nicht drueckt, haelt die Karte
|
||
fuer eine Anzeige. Der Name sagt jetzt, was passiert. */
|
||
const mehr = el('button', 'knopf-still knopf-still--bearbeiten',
|
||
offen.has(l.id) ? 'Fertig' : 'Bearbeiten');
|
||
mehr.type = 'button';
|
||
mehr.setAttribute('aria-expanded', offen.has(l.id) ? 'true' : 'false');
|
||
mehr.addEventListener('click', () => {
|
||
if (offen.has(l.id)) offen.delete(l.id); else offen.add(l.id);
|
||
zeichne();
|
||
});
|
||
knöpfe.append(mehr);
|
||
k.append(knöpfe);
|
||
|
||
if (!offen.has(l.id)) return k;
|
||
|
||
/* ---------- Aufgeklappt: Qualifizierung und Pflege ---------- */
|
||
const d = el('div', 'kk__detail');
|
||
|
||
/* AKTIVITAET UND POTENZIAL WAREN NUR ZU LESEN (11.09.2026).
|
||
|
||
Sie liessen sich ausschliesslich beim ANLEGEN eintragen -- danach
|
||
standen sie da und waren nicht mehr zu aendern. Genau das meinte
|
||
Filipes "man kan da nichts machen": Es sah nach Anzeige aus, weil
|
||
es Anzeige war. Jetzt sind es Felder wie alle anderen. */
|
||
const notiz = el('textarea', 'kk__notiz');
|
||
notiz.rows = 3;
|
||
notiz.maxLength = 3000;
|
||
notiz.placeholder = 'Notizen zum Gespräch – was wurde besprochen, was ist offen?';
|
||
notiz.value = l.notizen || '';
|
||
d.append(el('label', 'feld-schild', 'Notizen'), notiz);
|
||
|
||
const raster = el('div', 'kk__raster');
|
||
const mach = (schild, typ, wert, aufbau) => {
|
||
const box = el('div', 'feld');
|
||
box.append(el('label', 'feld-schild', schild));
|
||
const f = typ === 'select' ? el('select') : el('input');
|
||
if (typ !== 'select') f.type = typ;
|
||
if (aufbau) aufbau(f);
|
||
f.value = wert || '';
|
||
box.append(f);
|
||
raster.append(box);
|
||
return f;
|
||
};
|
||
const fNachricht = mach('Zuletzt geschrieben', 'date', l.letzte_nachricht);
|
||
const fFollowup = mach('Nächster Follow-up', 'date', l.naechster_followup);
|
||
const fPrio = mach('Priorität', 'select', l.prioritaet, (f) => {
|
||
for (const [w, t] of [['hoch', 'Hoch'], ['mittel', 'Mittel'], ['niedrig', 'Niedrig']]) {
|
||
const o = document.createElement('option'); o.value = w; o.textContent = t; f.append(o);
|
||
}
|
||
});
|
||
const fStufe = mach('Stufe', 'select', l.status, (f) => {
|
||
for (const [w, t] of Object.entries(stufen)) {
|
||
const o = document.createElement('option'); o.value = w; o.textContent = t; f.append(o);
|
||
}
|
||
});
|
||
|
||
/* ---- DIE SIEBEN NEUEN FELDER ------------------------------------
|
||
|
||
Die Auswahllisten kommen vom Server (laender, netzwerk) -- hier
|
||
steht keine eigene. */
|
||
const leer = (f, text) => {
|
||
const o = document.createElement('option'); o.value = ''; o.textContent = text; f.append(o);
|
||
};
|
||
const fNetz = mach('Schon im Netzwerk?', 'select', l.netzwerk, (f) => {
|
||
leer(f, '– nicht angegeben –');
|
||
for (const w of netzwerkListe) {
|
||
const o = document.createElement('option');
|
||
o.value = w; o.textContent = netzwerkName[w] || w; f.append(o);
|
||
}
|
||
});
|
||
const fLand = mach('Land', 'select', l.land, (f) => {
|
||
leer(f, '– nicht angegeben –');
|
||
for (const w of laenderListe) {
|
||
const o = document.createElement('option');
|
||
o.value = w; o.textContent = landName[w] || w; f.append(o);
|
||
}
|
||
});
|
||
const fFollower = mach('Follower', 'text', l.follower ? String(l.follower) : '', (f) => {
|
||
f.placeholder = 'z. B. 12400 oder 12,4k';
|
||
f.inputMode = 'numeric';
|
||
});
|
||
const fWoher = mach('Wie gefunden', 'text', l.woher, (f) => {
|
||
f.maxLength = 80; f.placeholder = 'LIVE entdeckt, Empfehlung, Kommentar …';
|
||
});
|
||
const fWeg = mach('Angeschrieben über', 'text', l.kontaktweg, (f) => {
|
||
f.maxLength = 80; f.placeholder = 'TikTok-DM, Instagram, WhatsApp …';
|
||
});
|
||
const fZeiten = mach('LIVE-Zeiten', 'text', l.live_zeiten, (f) => {
|
||
f.maxLength = 200; f.placeholder = 'z. B. Mo–Fr ab 21 Uhr';
|
||
});
|
||
d.append(raster);
|
||
|
||
/* Die beiden langen Texte stehen UNTER dem Raster, nicht darin:
|
||
Sie brauchen Zeilen, keine Spalte. */
|
||
const lang = (schild, wert, platzhalter) => {
|
||
const t = el('textarea', 'kk__notiz');
|
||
t.rows = 2; t.maxLength = 3000; t.placeholder = platzhalter; t.value = wert || '';
|
||
d.append(el('label', 'feld-schild', schild), t);
|
||
return t;
|
||
};
|
||
const fAktiv = lang('Aktivität & LIVE-Fokus', l.aktivitaet,
|
||
'Wie oft LIVE? Welche Zeiten? Welches Format?');
|
||
const fPot = lang('Potenzial & Stil', l.potenzial,
|
||
'Was macht die Person besonders?');
|
||
/* Der Absagegrund erscheint nur, wo er hingehoert. Ein Feld
|
||
"warum nicht" an einem Kontakt, der gerade gut laeuft, ist eine
|
||
Frage, die niemand gestellt hat. */
|
||
const fAbsage = l.status === 'abgelehnt'
|
||
? lang('Warum es nicht gepasst hat', l.absage_grund,
|
||
'Kein Interesse, schon vergeben, passt nicht zur Agentur …')
|
||
: null;
|
||
|
||
const unten = el('div', 'kk__knoepfe');
|
||
const sp = el('button', 'knopf knopf--klein', 'Speichern');
|
||
sp.type = 'button';
|
||
sp.addEventListener('click', async () => {
|
||
sp.disabled = true;
|
||
const r = await schicke('/workspace/api/leads/' + l.id, 'PATCH', {
|
||
notizen: notiz.value, letzte_nachricht: fNachricht.value,
|
||
naechster_followup: fFollowup.value, prioritaet: fPrio.value, status: fStufe.value,
|
||
netzwerk: fNetz.value, land: fLand.value, follower: fFollower.value,
|
||
woher: fWoher.value, kontaktweg: fWeg.value, live_zeiten: fZeiten.value,
|
||
aktivitaet: fAktiv.value, potenzial: fPot.value,
|
||
...(fAbsage ? { absage_grund: fAbsage.value } : {}),
|
||
});
|
||
sp.disabled = false;
|
||
if (r) await laden();
|
||
});
|
||
unten.append(sp);
|
||
|
||
if (l.status !== 'abgelehnt' && l.status !== 'uebergeben') {
|
||
const ab = el('button', 'knopf-still', 'Passt nicht');
|
||
ab.type = 'button';
|
||
ab.addEventListener('click', () => stufeSetzen(l, 'abgelehnt'));
|
||
unten.append(ab);
|
||
}
|
||
const wg = el('button', 'knopf-still knopf-still--warn', 'Löschen');
|
||
wg.type = 'button';
|
||
wg.addEventListener('click', async () => {
|
||
if (!await frageNach({
|
||
titel: `„${l.name}“ aus der Pipeline entfernen?`,
|
||
was: 'Der Lead verschwindet aus der Liste, samt Notizen und Verlauf.',
|
||
bleibt: 'Andere Leads bleiben unberührt.',
|
||
endgueltig: true, ja: 'Entfernen',
|
||
})) return;
|
||
if (await schicke('/workspace/api/leads/' + l.id, 'DELETE')) { offen.delete(l.id); await laden(); }
|
||
});
|
||
unten.append(wg);
|
||
d.append(unten);
|
||
|
||
k.append(d);
|
||
return k;
|
||
}
|
||
|
||
async function stufeSetzen(l, status) {
|
||
if (await schicke('/workspace/api/leads/' + l.id, 'PATCH', { status })) await laden();
|
||
}
|
||
|
||
async function onboarding(l, knopf) {
|
||
if (!await frageNach({
|
||
titel: `Aus „${l.name}“ einen Creator mit eigenem Zugang anlegen?`,
|
||
was: 'Es entsteht ein Konto mit eigenem Zugangscode.',
|
||
bleibt: 'Der Lead bleibt in der Pipeline stehen und wird als '
|
||
+ 'übernommen markiert.',
|
||
ja: 'Creator anlegen',
|
||
})) return;
|
||
knopf.disabled = true;
|
||
const r = await schicke('/workspace/api/leads/' + l.id + '/onboarding', 'POST', { name: l.name });
|
||
knopf.disabled = false;
|
||
if (!r) return;
|
||
$('code-wert').textContent = r.code;
|
||
$('code-schleier').hidden = false;
|
||
$('code-zu').focus();
|
||
await laden();
|
||
}
|
||
|
||
/* ---------- Zeichnen ----------------------------------------------------- */
|
||
|
||
function zeichne() {
|
||
const ziel = $('pipeline');
|
||
ziel.textContent = '';
|
||
ziel.setAttribute('aria-busy', 'false');
|
||
|
||
const scout = $('f-scout').value;
|
||
const liste = scout ? daten.filter((l) => String(l.scout_id) === scout) : daten;
|
||
|
||
/* Follow-ups, die heute oder frueher faellig sind. */
|
||
const fällig = liste.filter((l) => l.naechster_followup && l.naechster_followup <= heute
|
||
&& l.status !== 'uebergeben' && l.status !== 'abgelehnt');
|
||
$('faellig').hidden = fällig.length === 0;
|
||
const fl = $('faellig-liste');
|
||
fl.textContent = '';
|
||
for (const l of fällig) {
|
||
const li = el('li');
|
||
const b = el('button', 'faellig__knopf', l.name);
|
||
b.type = 'button';
|
||
b.addEventListener('click', () => {
|
||
offen.add(l.id); zeichne();
|
||
document.getElementById('lead-' + l.id)?.scrollIntoView({ block: 'center', behavior: window.sanft?.() || 'auto' });
|
||
});
|
||
li.append(b, el('span', 'kk__still', datum(l.naechster_followup)));
|
||
fl.append(li);
|
||
}
|
||
|
||
if (!liste.length) {
|
||
/* WOFUER IST DAS GUT? (Wunsch vom 01.09.2026)
|
||
|
||
Vorher stand hier ein Satz, der nur wiederholte, was man
|
||
ohnehin sieht: dass nichts da ist. Wer die Seite zum ersten
|
||
Mal oeffnet, weiss danach immer noch nicht, warum es sie gibt.
|
||
|
||
Der leere Zustand ist der EINZIGE Moment, in dem jemand
|
||
garantiert liest, was dort steht -- spaeter ist die Flaeche
|
||
von Daten belegt. Deshalb steht die Erklaerung genau hier und
|
||
nicht in einer Hilfe, die niemand aufmacht.
|
||
|
||
Erklaert wird der NUTZEN, nicht die Bedienung: nicht "hier
|
||
klicken", sondern warum ein Scout ohne diese Liste Leute
|
||
verliert. */
|
||
const kasten = el('div', 'leer-hinweis leer-hinweis--erklaerung');
|
||
|
||
kasten.append(el('p', 'erklaerung__satz',
|
||
'Hier steht jeder, den ihr entdeckt habt – von der ersten Nachricht '
|
||
+ 'bis zur Übergabe an einen Manager.'));
|
||
|
||
kasten.append(el('p', 'erklaerung__warum',
|
||
'Wozu das gut ist: Ein Scout spricht viele an. Ohne Liste erinnert '
|
||
+ 'sich niemand, wer schon gefragt wurde, wer zugesagt hat und bei '
|
||
+ 'wem seit drei Wochen nichts passiert ist. Genau die gehen '
|
||
+ 'verloren – nicht die, die Nein sagen.'));
|
||
|
||
/* Aus derselben Liste wie die Stufenkoepfe -- siehe STUFE_WAS.
|
||
"Abgelehnt" bleibt hier weg: Die Einfuehrung beschreibt den
|
||
Weg nach vorn, und eine Absage ist keine Stufe darauf. */
|
||
const stufenListe = el('ol', 'erklaerung__stufen');
|
||
for (const [nr, name, was] of [
|
||
['1', 'Neu', STUFE_WAS.neu],
|
||
['2', 'Angesprochen', STUFE_WAS.angesprochen],
|
||
['3', 'Gespräch', STUFE_WAS.gespraech],
|
||
['4', 'Interessiert', STUFE_WAS.interessiert],
|
||
['5', 'Übergeben', STUFE_WAS.uebergeben],
|
||
]) {
|
||
const z = el('li', 'erklaerung__stufe');
|
||
z.append(el('span', 'erklaerung__nr', nr));
|
||
const t = el('div', null);
|
||
t.append(el('strong', null, name));
|
||
t.append(el('span', 'erklaerung__was', was));
|
||
z.append(t);
|
||
stufenListe.append(z);
|
||
}
|
||
kasten.append(stufenListe);
|
||
|
||
kasten.append(el('p', 'erklaerung__fuss',
|
||
'Zu jedem Kontakt gehört ein Datum für den nächsten Schritt. Ist es '
|
||
+ 'überfällig, erscheint der Kontakt auf deiner Startseite – du musst '
|
||
+ 'also nicht daran denken, die Seite denkt für dich.'));
|
||
|
||
kasten.append(el('p', 'erklaerung__los',
|
||
'„Neuer Kontakt“ oben rechts legt den ersten an.'));
|
||
|
||
ziel.append(kasten);
|
||
return;
|
||
}
|
||
|
||
for (const [wert, name] of Object.entries(stufen)) {
|
||
const drin = liste.filter((l) => l.status === wert);
|
||
if (!drin.length) continue;
|
||
const gruppe = el('section', 'stufe');
|
||
gruppe.dataset.stufe = wert;
|
||
/* JEDE STUFE SAGT, WAS SIE BEDEUTET (11.09.2026).
|
||
|
||
Die Erklaerung stand bisher nur im leeren Zustand -- also
|
||
genau einmal, bevor der erste Kontakt da war, und danach nie
|
||
wieder. Wer im Januar dazukommt, sieht sechs Ueberschriften und
|
||
muss raten, was "Interessiert" von "Gespraech" unterscheidet.
|
||
|
||
Der Satz steht deshalb an der Stufe selbst und wird aus
|
||
derselben Liste gelesen wie der leere Zustand -- nicht aus
|
||
einer zweiten. */
|
||
const kopf = el('div', 'stufe__kopf');
|
||
const kt = el('div', 'stufe__text');
|
||
kt.append(el('h2', 'stufe__titel', name));
|
||
if (STUFE_WAS[wert]) kt.append(el('p', 'stufe__was', STUFE_WAS[wert]));
|
||
kopf.append(kt, el('span', 'stufe__zahl', String(drin.length)));
|
||
gruppe.append(kopf);
|
||
const box = el('div', 'stufe__karten');
|
||
for (const l of drin) {
|
||
const k = karte(l);
|
||
k.id = 'lead-' + l.id;
|
||
box.append(k);
|
||
}
|
||
gruppe.append(box);
|
||
ziel.append(gruppe);
|
||
}
|
||
}
|
||
|
||
async function laden() {
|
||
try {
|
||
const a = await hole('/workspace/api/leads');
|
||
if (!a.ok) { melde('Pipeline konnte nicht geladen werden.'); return; }
|
||
const d = await a.json();
|
||
stufen = d.stufen; heute = d.heute; daten = d.leads;
|
||
laenderListe = d.laender || []; landName = d.land_name || {};
|
||
netzwerkListe = d.netzwerk || []; netzwerkName = d.netzwerk_name || {};
|
||
zeichne();
|
||
} catch { /* umgeleitet */ }
|
||
}
|
||
|
||
/* ---------- Neuer Kontakt ------------------------------------------------ */
|
||
|
||
$('neu-auf').addEventListener('click', () => {
|
||
$('neu-block').hidden = false;
|
||
$('neu-auf').hidden = true;
|
||
$('n-name').focus();
|
||
});
|
||
$('neu-zu').addEventListener('click', () => {
|
||
$('neu-block').hidden = true;
|
||
$('neu-auf').hidden = false;
|
||
$('neu-form').reset();
|
||
$('n-fehler').textContent = '';
|
||
});
|
||
|
||
$('neu-form').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
$('n-fehler').textContent = '';
|
||
const knopf = $('n-speichern');
|
||
knopf.disabled = true;
|
||
const a = await hole('/workspace/api/leads', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
name: $('n-name').value, plattform: $('n-plattform').value,
|
||
handle: $('n-handle').value, prioritaet: $('n-prio').value,
|
||
aktivitaet: $('n-aktivitaet').value, potenzial: $('n-potenzial').value,
|
||
naechster_followup: $('n-followup').value,
|
||
scout_id: $('n-scout').value || null,
|
||
netzwerk: $('n-netzwerk').value, land: $('n-land').value,
|
||
follower: $('n-follower').value,
|
||
}),
|
||
}).catch(() => null);
|
||
knopf.disabled = false;
|
||
if (!a) return;
|
||
if (!a.ok) {
|
||
$('n-fehler').textContent = sagWas((await a.json().catch(() => ({}))).fehler, 'Ging nicht.');
|
||
return;
|
||
}
|
||
$('neu-zu').click();
|
||
await laden();
|
||
});
|
||
|
||
/* ---------- Codefenster --------------------------------------------------- */
|
||
|
||
$('code-zu').addEventListener('click', () => {
|
||
$('code-schleier').hidden = true;
|
||
$('code-wert').textContent = '—';
|
||
});
|
||
$('code-kopieren').addEventListener('click', async () => {
|
||
try {
|
||
await navigator.clipboard.writeText($('code-wert').textContent);
|
||
$('code-kopieren').textContent = 'Kopiert ✓';
|
||
setTimeout(() => { $('code-kopieren').textContent = 'Kopieren'; }, 2200);
|
||
} catch { /* ohne Zwischenablage bleibt der Code lesbar stehen */ }
|
||
});
|
||
|
||
/* ---------- Start --------------------------------------------------------- */
|
||
|
||
(async () => {
|
||
try {
|
||
const a = await hole('/workspace/api/ich');
|
||
if (!a.ok) { location.assign('/workspace/'); return; }
|
||
ich = await a.json();
|
||
} catch {
|
||
/* NICHT MEHR STUMM (20.09.2026). Hier stand `catch { return; }`
|
||
-- und damit blieb die Seite bei jedem Netzaussetzer fuer
|
||
immer auf "wird geladen …" stehen. Gemessen auf 13 von 21
|
||
Seiten. `ladefehler()` sagt, was los ist, und bietet einen
|
||
Knopf an, der es noch einmal versucht. */
|
||
window.ladefehler?.();
|
||
return;
|
||
}
|
||
if (ich.rolle === 'creator') { location.assign('/workspace/start.html'); return; }
|
||
window.werZeigen?.(ich);
|
||
|
||
if (LEITUNG.has(ich.rolle)) {
|
||
$('unterzeile').textContent = 'Gesamtübersicht über alle Scouts.';
|
||
try {
|
||
const { scouts } = await (await hole('/workspace/api/leads-scouts')).json();
|
||
for (const s of scouts) {
|
||
for (const wo of [$('f-scout'), $('n-scout')]) {
|
||
const o = document.createElement('option');
|
||
o.value = String(s.id); o.textContent = s.name;
|
||
wo.append(o);
|
||
}
|
||
}
|
||
$('scoutwahl-block').hidden = false;
|
||
$('n-scout-block').hidden = false;
|
||
} catch { return; }
|
||
$('f-scout').addEventListener('change', zeichne);
|
||
} else {
|
||
$('unterzeile').textContent = 'Deine Pipeline – nur du und DogFather sehen sie.';
|
||
}
|
||
|
||
await laden();
|
||
})();
|
||
})();
|