Filipe hat drei Sachen gemeldet. Zwei davon waren nicht das, wonach sie
aussahen.
1. "BEI DER SPICY ROLLE IST DA EIN PROBLEM" (Creator-Profil laedt nicht)
Nicht diese Seite war kaputt. In NEUN Skripten stand dieselbe Zeile
const LEITUNG = new Set(['admin', 'manager']);
und in keinem davon 'spicy'. profil.js nahm deshalb den Creator-Zweig
und lud das EIGENE Profil -- ein Spicy-Zugang ist kein Creator, also
404. Acht der neun sind nicht kaputtgegangen; sie haben sich still
falsch verhalten.
Die Liste steht jetzt EINMAL in bereiche.js. Beim Aufraeumen kamen mit
derselben Suche sechs weitere Luecken heraus, alle vom selben Typ:
* Aufgaben abbrechen -- Server UND Knopf ohne 'spicy'
* Teilen-Knopf in der Kopfleiste -- ohne 'spicy'
* Chat: die Gespraechsliste laeuft ueber eine feste Rollenfolge.
Wer nicht darin steht, taucht gar nicht auf -- eine Person, die es
fuer die anderen nicht gibt.
* Kalender: dieselbe Rollenfolge, dort landete Spicy Media durch
indexOf() === -1 ganz oben statt an ihrem Platz.
* Dashboard: "Creator anlegen" nur fuer admin/manager -- der Server
haette sie gelassen, den Knopf hat sie nie gesehen.
* Personenliste: ROLLENNAME ohne 'spicy'. Der Auffangwert
`|| p.rolle` schrieb "spicy" statt "Spicy Media" -- plausibel
genug, um jahrelang zu bleiben.
`pruef-css-klassen.mjs` verlangt jetzt, dass kein Workspace-Skript sich
wieder eine eigene Rollenliste baut. Mit Gegenprobe, dass der Sucher so
eine Liste auch wirklich findet.
2. "DIE ZAHLEN DIESE KATEGORIE NICHT SEHEN"
Kachel und Seite waren beim Anlegen der Rolle auf ["spicy","admin"]
gesetzt worden -- "dieselben Rechte ausser Automationen". Beides jetzt
auf DogFather allein.
3. "IN CIGDEMS KALENDER STEHT IMMER NOCH MEIN MANAGER MEETING"
Das war MEIN Denkfehler von heute Frueh, nicht ein vergessener Fall.
Ich hatte "geht alle an" als "kein Teilnehmer eingetragen" definiert --
also entschied der Server, was alle angeht, und nicht der, der den
Termin anlegt. Wer niemanden eintraegt, meint aber meistens nicht
"alle", sondern "mich".
Der Fall ist entfallen. Sichtbar ist ein Termin jetzt nur noch fuer
den, der ihn angelegt hat, dem er zugeordnet ist, der das Gegenueber
ist -- oder der in der Teilnehmerliste steht. Damit ist das Markieren
im Termin die einzige Antwort auf "wen geht das an": eine sichtbare
Entscheidung im Formular statt einer unsichtbaren Regel im Server.
Calls & Protokolle rufen dieselbe Funktion auf und koennen deshalb gar
nicht auseinanderlaufen -- das war Filipes vierter Wunsch, und er ist
mit derselben Zeile erledigt.
Am laufenden System als Cigdem nachgemessen: Profil laedt ("Profil:
Luna"), Zahlen-Kachel weg und Seite gesperrt, im Kalender NUR der
Termin, bei dem sie markiert ist, Calls zeigt genau denselben.
Fuenf Pruefungen auf die endgueltige Regel umgeschrieben statt
geloescht (sicht, spicy, teilnehmer, scout-zuteilung, css-klassen).
Die dritte Drehung bei scout-zuteilung steht mit allen drei Staenden
in der Akte -- eine geloeschte Pruefung hinterlaesst keine Spur davon,
dass hier einmal etwas anderes galt.
Co-Authored-By: Claude Opus 5 <[email protected]>
425 lines
16 KiB
JavaScript
425 lines
16 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',
|
||
};
|
||
|
||
let ich = null;
|
||
let stufen = {};
|
||
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((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;
|
||
}
|
||
|
||
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');
|
||
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');
|
||
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);
|
||
}
|
||
const mehr = el('button', 'knopf-still', offen.has(l.id) ? 'Weniger' : 'Details');
|
||
mehr.type = 'button';
|
||
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');
|
||
|
||
if (l.aktivitaet) d.append(zeile('Aktivität & LIVE-Fokus', l.aktivitaet));
|
||
if (l.potenzial) d.append(zeile('Potenzial & Stil', l.potenzial));
|
||
|
||
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);
|
||
}
|
||
});
|
||
d.append(raster);
|
||
|
||
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,
|
||
});
|
||
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 (!confirm(`„${l.name}" wirklich aus der Pipeline 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 (!confirm(`Aus „${l.name}" jetzt einen Creator mit eigenem Zugang 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: 'smooth' });
|
||
});
|
||
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.'));
|
||
|
||
const stufenListe = el('ol', 'erklaerung__stufen');
|
||
for (const [nr, name, was] of [
|
||
['1', 'Neu', 'Entdeckt. Noch nicht angeschrieben.'],
|
||
['2', 'Angesprochen', 'Erste Nachricht ist raus. Jetzt läuft die Uhr.'],
|
||
['3', 'Gespräch', 'Ihr seid im Austausch – Fragen, Bedingungen, Termine.'],
|
||
['4', 'Interessiert', 'Grundsätzlich dabei. Jetzt zählt, dass es nicht liegen bleibt.'],
|
||
['5', 'Übergeben', 'Ein Manager übernimmt. Ab hier ist es Betreuung, kein Scouting mehr.'],
|
||
]) {
|
||
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;
|
||
const kopf = el('div', 'stufe__kopf');
|
||
kopf.append(el('h2', 'stufe__titel', name), 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;
|
||
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,
|
||
}),
|
||
}).catch(() => null);
|
||
knopf.disabled = false;
|
||
if (!a) return;
|
||
if (!a.ok) {
|
||
$('n-fehler').textContent = (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 { 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();
|
||
})();
|
||
})();
|