Files
dogfather-universe/workspace/assets/js/scouting.js
T
DogFatherGit 1c3e642f12 Workspace: Rolle "Management" heisst jetzt "DogFather"
Auf Wunsch von Filipe. Betrifft ausschliesslich die Anzeige.

Der Rollenschluessel bleibt "admin". Er steckt in der CHECK-Regel der
Datenbank, in jeder bestehenden Sitzung und in jeder Rechteabfrage --
ihn umzubenennen haette alle drei gebrochen, und bestehende Anmeldungen
waeren ungueltig geworden. Umbenannt wird nur, was man LIEST.

Der Anzeigename steht jetzt an EINER Stelle (ROLLEN_NAME in
workspace.js) und kommt ueber /workspace/api/ich als rolle_name mit.
Stand er in elf Dateien, waere er beim naechsten Mal in zehn davon
geaendert.

Dabei aufgefallen: In der Kopfleiste stand auf JEDER Seite der rohe
Rollenschluessel -- "Chef · admin". Das war schon vorher unschoen, faellt
aber jetzt erst richtig auf. Alle elf Seiten zeigen jetzt "Chef ·
DogFather". Geprueft: kein rohes "admin" mehr in der Oberflaeche.

Geaendert: Rollenkachel und Untertitel auf der Anmeldeseite, Kopfleiste
aller Seiten, Rollentext auf dem Dashboard, Rollenmarken und Auswahl in
der Personenverwaltung, die Betreuungs-Auswahl ("nur DogFather"), der
Hinweis auf der Dateienseite, die Erklaerung zur internen Notiz im
Profil, die Hinweise fuer Scouts ohne Zuteilung -- und vier
Fehlermeldungen vom Server.

Schreibweise "DogFather" wie von Filipe geschrieben; das ist auch auf
der oeffentlichen Website die haeufigste Form (717 von 1216).

In den Code-Kommentaren der Fachmodule heisst die Rolle weiterhin "das
Management". Das bleibt bewusst so -- eine Massenaenderung an vierzig
Kommentaren waere reines Risiko ohne sichtbaren Nutzen. Ein Hinweis an
der ROLLEN_NAME-Zuordnung erklaert den Zusammenhang.
2026-08-28 11:55:05 +02:00

367 lines
13 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.
/* ===================================================================
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';
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 && ich.rolle === 'admin') 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' && ich.rolle === 'admin' && !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) {
ziel.append(el('p', 'leer-hinweis',
'Noch niemand in der Pipeline. „Neuer Kontakt" legt den ersten an.'));
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; }
$('wer').textContent = ich.name + ' · ' + (ich.rolle_name || ich.rolle);
if (ich.rolle === 'admin') {
$('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();
})();
})();