Workspace: Scout-CRM -- Pipeline, Follow-ups und Uebergabe
/workspace/scouting.html. Erster Baustein aus Phase 3 und der Bereich,
in dem Scouts bisher praktisch nichts hatten.
Der Merksatz von Seite 15 ist hier die Sicherheitsregel, nicht nur eine
Beschreibung: "Scouts sehen ihre Pipeline -- die Admin-Rolle die
Gesamtuebersicht -- Creator keine Scout-internen Daten."
- Creator bekommen 404, nicht 403, und zwar auf Schnittstelle UND Seite.
Sie sollen nicht einmal erfahren, dass es den Bereich gibt.
- Ein Scout sieht ausschliesslich die eigene Pipeline. Geprueft: Patrick
bekommt auf Sams Lead 404 beim Lesen, Aendern und Loeschen.
- Das Management sieht alles, kann nach Scout filtern und Leads einem
Scout zuordnen.
Pipeline als gruppierte Liste, nicht als Board: Sechs Spalten waeren auf
dem Handy unbedienbar, und Scouts arbeiten unterwegs. Jede Stufe traegt
eine eigene Farbe an der linken Kante, die von kuehl (neu entdeckt) nach
gruen (uebergeben) laeuft -- die Richtung der Pipeline wird sichtbar,
ohne Ampel-Geblinke.
Jede Karte hat genau EINEN naheliegenden Schritt ("Weiter zu ..."), der
Rest steckt im aufklappbaren Teil. Der Weiter-Knopf traegt bewusst nur
die Farbe seiner Stufe statt des vollen Farbverlaufs, sonst waere die
Seite ein Streifenmuster aus identischen Leuchtbalken.
Faellige Follow-ups stehen als eigene Leiste ganz oben. Das ist der
Teil, der ohne System am ehesten untergeht -- nicht der Kontakt selbst,
sondern das Nachfassen.
Uebergabe ("Creator-Onboarding starten", Seite 15): Aus einem
uebergebenen Lead legt das Management mit einem Klick eine echte Person
mit Zugangscode an. Der Code wird genau einmal angezeigt, wie in der
Personenverwaltung. Die Qualifizierung des Scouts (Plattform, Handle,
Potenzial, LIVE-Aktivitaet) wandert dabei automatisch ins Creator-Profil
-- sonst muesste das Management abtippen, was laengst dasteht. Geprueft:
Lead -> Person -> Profil, alle drei verbunden, zweiter Versuch wird
abgelehnt.
Ein uebergebener Lead ist Teil der Betreuungsgeschichte -- den loescht
nur das Management, nicht der Scout (403).
Nebenbei: .knopf-still gab es im Baukasten noch gar nicht, die Zweit-
knoepfe waren nackte Systemknoepfe. Jetzt sauber definiert.
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
/* ===================================================================
|
||||
Scout-CRM.
|
||||
|
||||
Die Pipeline hat eine Richtung, das soll man sehen: Jede Stufe traegt
|
||||
links eine farbige Kante, die von kuehl (neu entdeckt) zu warm/gruen
|
||||
(uebergeben) laeuft. Gedeckt gehalten -- kein Ampel-Geblinke.
|
||||
=================================================================== */
|
||||
|
||||
.steuerung { display: flex; gap: 14px; flex-wrap: wrap; align-items: flex-end; }
|
||||
.steuerung select {
|
||||
min-width: 160px; padding: 9px 12px;
|
||||
background: rgba(255, 255, 255, .045);
|
||||
border: 1px solid var(--rand); border-radius: var(--radius-klein);
|
||||
color: var(--text); font: inherit;
|
||||
}
|
||||
|
||||
/* ---------- Fällige Follow-ups ------------------------------------------ */
|
||||
|
||||
.faellig {
|
||||
margin-bottom: 26px; padding: 14px 18px;
|
||||
background: rgba(255, 190, 120, .07);
|
||||
border: 1px solid rgba(255, 190, 120, .3);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
.faellig__titel {
|
||||
margin: 0 0 8px; font-size: .74rem; font-weight: 700;
|
||||
letter-spacing: .08em; text-transform: uppercase; color: #f0c48a;
|
||||
}
|
||||
.faellig__liste { display: flex; flex-wrap: wrap; gap: 8px 16px; margin: 0; padding: 0; list-style: none; }
|
||||
.faellig__liste li { display: flex; align-items: baseline; gap: 7px; }
|
||||
.faellig__knopf {
|
||||
padding: 0; background: none; border: 0; cursor: pointer;
|
||||
color: var(--text); font: inherit; font-weight: 600;
|
||||
border-bottom: 1px solid rgba(240, 196, 138, .45);
|
||||
}
|
||||
.faellig__knopf:hover { color: #f0c48a; }
|
||||
|
||||
/* ---------- Stufen -------------------------------------------------------- */
|
||||
|
||||
.stufe { margin-bottom: 30px; }
|
||||
.stufe__kopf { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
|
||||
.stufe__titel {
|
||||
margin: 0; font-size: .78rem; font-weight: 700;
|
||||
letter-spacing: .08em; text-transform: uppercase; color: var(--akzent);
|
||||
}
|
||||
.stufe__zahl {
|
||||
min-width: 22px; padding: 1px 7px; text-align: center;
|
||||
font-size: .74rem; font-weight: 700; color: var(--text-still);
|
||||
background: rgba(255, 255, 255, .06);
|
||||
border-radius: 999px;
|
||||
}
|
||||
.stufe__karten { display: grid; gap: 10px; }
|
||||
|
||||
/* Farbverlauf entlang der Pipeline. */
|
||||
.stufe[data-stufe="neu"] { --stufe-farbe: #4c7fa8; }
|
||||
.stufe[data-stufe="angesprochen"] { --stufe-farbe: #5b8fd4; }
|
||||
.stufe[data-stufe="gespraech"] { --stufe-farbe: #7d84e0; }
|
||||
.stufe[data-stufe="interessiert"] { --stufe-farbe: #8ab8a0; }
|
||||
.stufe[data-stufe="uebergeben"] { --stufe-farbe: #79d1a2; }
|
||||
.stufe[data-stufe="abgelehnt"] { --stufe-farbe: #55606f; }
|
||||
.stufe[data-stufe="abgelehnt"] .kk { opacity: .62; }
|
||||
.stufe[data-stufe="abgelehnt"] .kk:hover { opacity: 1; }
|
||||
|
||||
/* ---------- Karte --------------------------------------------------------- */
|
||||
|
||||
.kk {
|
||||
position: relative;
|
||||
padding: 15px 18px 15px 21px;
|
||||
background: rgba(255, 255, 255, .03);
|
||||
border: 1px solid var(--rand);
|
||||
border-radius: var(--radius-klein);
|
||||
transition: border-color .18s ease, background .18s ease, opacity .18s ease;
|
||||
}
|
||||
.kk::before {
|
||||
content: ""; position: absolute; left: 0; top: 10px; bottom: 10px; width: 3px;
|
||||
border-radius: 0 3px 3px 0;
|
||||
background: var(--stufe-farbe, var(--akzent));
|
||||
opacity: .8;
|
||||
}
|
||||
.kk:hover { border-color: rgba(255, 255, 255, .16); }
|
||||
.kk[data-faellig="ja"] { border-color: rgba(255, 190, 120, .34); }
|
||||
|
||||
.kk__kopf { display: flex; gap: 14px; align-items: flex-start; justify-content: space-between; flex-wrap: wrap; }
|
||||
.kk__name { margin: 0; font-size: 1.02rem; font-weight: 650; }
|
||||
.kk__unter { margin: 2px 0 0; font-size: .82rem; color: var(--text-still); }
|
||||
|
||||
.kk__marken { display: flex; gap: 6px; flex-wrap: wrap; }
|
||||
.marke-punkt, .marke-still, .marke-fertig {
|
||||
padding: 3px 9px; border-radius: 999px;
|
||||
font-size: .71rem; font-weight: 600; letter-spacing: .02em;
|
||||
border: 1px solid var(--rand); color: var(--text-still);
|
||||
}
|
||||
.marke-punkt[data-prio="hoch"] {
|
||||
color: #ffb4b9; border-color: rgba(255, 154, 162, .38); background: rgba(255, 154, 162, .1);
|
||||
}
|
||||
.marke-punkt[data-prio="mittel"] { color: var(--text-leise); }
|
||||
.marke-fertig {
|
||||
color: #79d1a2; border-color: rgba(121, 209, 162, .35); background: rgba(121, 209, 162, .09);
|
||||
}
|
||||
|
||||
.kk__info { display: flex; flex-wrap: wrap; gap: 6px 14px; margin-top: 10px; font-size: .78rem; }
|
||||
.kk__still { color: var(--text-still); }
|
||||
.kk__followup { color: var(--text-leise); }
|
||||
.kk__followup[data-faellig="ja"] { color: #f0c48a; font-weight: 600; }
|
||||
|
||||
.kk__knoepfe { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-top: 13px; }
|
||||
|
||||
/* Der Weiter-Schritt wiederholt sich auf jeder Karte. Als voller
|
||||
Farbverlauf waere die Seite ein Streifenmuster -- er traegt deshalb
|
||||
nur die Farbe seiner Stufe und wird erst beim Zeigen kraeftiger. */
|
||||
.kk__knoepfe .knopf--weiter {
|
||||
background: color-mix(in srgb, var(--stufe-farbe) 15%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--stufe-farbe) 45%, transparent);
|
||||
color: color-mix(in srgb, var(--stufe-farbe) 60%, #ffffff);
|
||||
font-weight: 600;
|
||||
}
|
||||
.kk__knoepfe .knopf--weiter:hover {
|
||||
filter: none;
|
||||
background: color-mix(in srgb, var(--stufe-farbe) 26%, transparent);
|
||||
border-color: color-mix(in srgb, var(--stufe-farbe) 70%, transparent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Zurueckhaltender Zweitknopf -- gab es im Baukasten noch nicht. */
|
||||
.knopf-still {
|
||||
padding: 7px 13px; cursor: pointer;
|
||||
background: transparent;
|
||||
border: 1px solid var(--rand); border-radius: var(--radius-klein);
|
||||
color: var(--text-still); font: inherit; font-size: .82rem;
|
||||
transition: color .16s ease, border-color .16s ease, background .16s ease;
|
||||
}
|
||||
.knopf-still:hover {
|
||||
color: var(--text);
|
||||
border-color: rgba(255, 255, 255, .22);
|
||||
background: rgba(255, 255, 255, .04);
|
||||
}
|
||||
|
||||
/* Das Onboarding ist der einzige Schritt, der eine echte Person anlegt --
|
||||
der Knopf soll sich deshalb von den anderen abheben. */
|
||||
.knopf--gold {
|
||||
background: linear-gradient(100deg, rgba(240, 196, 138, .92), rgba(224, 160, 110, .92));
|
||||
color: #241608;
|
||||
border: 0;
|
||||
}
|
||||
.knopf--gold:hover { filter: brightness(1.07); }
|
||||
|
||||
/* ---------- Aufgeklappt ---------------------------------------------------- */
|
||||
|
||||
.kk__detail {
|
||||
margin-top: 15px; padding-top: 15px;
|
||||
border-top: 1px solid var(--rand);
|
||||
}
|
||||
.kk__zeile { margin: 0 0 9px; font-size: .87rem; color: var(--text-leise); }
|
||||
.kk__schild {
|
||||
display: block; margin-bottom: 1px;
|
||||
font-size: .7rem; font-weight: 700; letter-spacing: .07em;
|
||||
text-transform: uppercase; color: var(--text-still);
|
||||
}
|
||||
.kk__notiz {
|
||||
width: 100%; margin: 4px 0 14px; padding: 10px 12px;
|
||||
background: rgba(255, 255, 255, .04);
|
||||
border: 1px solid var(--rand); border-radius: var(--radius-klein);
|
||||
color: var(--text); font: inherit; resize: vertical;
|
||||
}
|
||||
.kk__raster {
|
||||
display: grid; gap: 12px; margin-bottom: 14px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
}
|
||||
.kk__raster input, .kk__raster select {
|
||||
width: 100%; padding: 9px 11px;
|
||||
background: rgba(255, 255, 255, .05);
|
||||
border: 1px solid var(--rand); border-radius: var(--radius-klein);
|
||||
color: var(--text); font: inherit;
|
||||
}
|
||||
.kk__notiz:focus, .kk__raster input:focus, .kk__raster select:focus {
|
||||
outline: none; border-color: rgba(63, 189, 245, .65);
|
||||
box-shadow: 0 0 0 3px rgba(63, 189, 245, .14);
|
||||
}
|
||||
.knopf-still--warn { color: #ffb4b9; border-color: rgba(255,154,162,.28); }
|
||||
.knopf-still--warn:hover { color: #ffd0d3; border-color: rgba(255,154,162,.5); background: rgba(255,154,162,.08); }
|
||||
|
||||
.leer-hinweis {
|
||||
padding: 30px; text-align: center; color: var(--text-still); font-size: .9rem;
|
||||
border: 1px dashed var(--rand); border-radius: var(--radius);
|
||||
}
|
||||
|
||||
/* ---------- Codefenster ---------------------------------------------------- */
|
||||
|
||||
.schleier {
|
||||
position: fixed; inset: 0; z-index: 60;
|
||||
display: grid; place-items: center; padding: 22px;
|
||||
background: rgba(3, 5, 10, .78);
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
.code-fenster {
|
||||
width: min(440px, 100%); padding: 26px 28px;
|
||||
background: #0b111b;
|
||||
border: 1px solid rgba(63, 189, 245, .3);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: 0 24px 60px -20px rgba(0, 0, 0, .8);
|
||||
}
|
||||
.code-fenster__titel { margin: 3px 0 10px; font-size: 1.2rem; font-weight: 700; }
|
||||
.code-fenster__text { margin: 0 0 16px; font-size: .85rem; color: var(--text-leise); }
|
||||
.code-fenster__code {
|
||||
margin: 0 0 20px; padding: 15px;
|
||||
font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
|
||||
font-size: 1.4rem; font-weight: 700; letter-spacing: .16em; text-align: center;
|
||||
word-break: break-all;
|
||||
background: rgba(63, 189, 245, .09);
|
||||
border: 1px solid rgba(63, 189, 245, .28);
|
||||
border-radius: var(--radius-klein);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.kk { transition: none; }
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
/* ===================================================================
|
||||
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;
|
||||
|
||||
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 das Management sehen sie.';
|
||||
}
|
||||
|
||||
await laden();
|
||||
})();
|
||||
})();
|
||||
@@ -30,7 +30,7 @@
|
||||
{ name: 'Schutz & Regeln', phase: 2, rollen: ['admin', 'creator'], ziel: 'bereich.html?b=schutz' },
|
||||
{ name: 'Reports & Review', phase: 2, rollen: ['admin', 'creator'], ziel: 'report.html' },
|
||||
{ name: 'Calls', phase: 3, rollen: ['admin', 'creator', 'scout'] },
|
||||
{ name: 'Scout-Pipeline', phase: 3, rollen: ['admin', 'scout'] },
|
||||
{ name: 'Scout-Pipeline', phase: 3, rollen: ['admin', 'scout'], ziel: 'scouting.html' },
|
||||
{ name: 'Automationen', phase: 4, rollen: ['admin'] },
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<title>Scouting · Creator Workspace</title>
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<meta name="theme-color" content="#05070d" />
|
||||
<link rel="icon" href="assets/img/favicon.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css" />
|
||||
<link rel="stylesheet" href="assets/css/start.css" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css" />
|
||||
<link rel="stylesheet" href="assets/css/scouting.css" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
|
||||
<header class="kopfleiste">
|
||||
<button type="button" class="zurueck-knopf" id="zurueck" hidden>
|
||||
<svg class="zurueck-knopf__pfeil" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M14.5 5.5 8 12l6.5 6.5" />
|
||||
</svg>
|
||||
<span class="zurueck-knopf__text">Zurück</span>
|
||||
</button>
|
||||
<p class="marke"><a class="zurueck" href="start.html">Creator Workspace</a> · Scouting</p>
|
||||
<div class="kopfleiste__rechts">
|
||||
<span class="wer" id="wer">…</span>
|
||||
<button type="button" class="abmelden" id="abmelden">Abmelden</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="inhalt inhalt--breit">
|
||||
|
||||
<section class="kopf-zeile">
|
||||
<div>
|
||||
<p class="marke">Scout-CRM</p>
|
||||
<h1 class="titel">Pipeline</h1>
|
||||
<p class="unterzeile" id="unterzeile">Vom ersten Fund bis zur Übergabe – nachvollziehbar statt im Kopf.</p>
|
||||
</div>
|
||||
<div class="steuerung">
|
||||
<div id="scoutwahl-block" hidden>
|
||||
<label class="feld-schild" for="f-scout">Scout</label>
|
||||
<select id="f-scout"><option value="">Alle Scouts</option></select>
|
||||
</div>
|
||||
<button type="button" class="knopf knopf--klein" id="neu-auf">Neuer Kontakt</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<p class="fehler" id="fehler" role="alert" aria-live="polite"></p>
|
||||
|
||||
<!-- Follow-ups: der Teil, der ohne System am ehesten untergeht. -->
|
||||
<section class="faellig" id="faellig" hidden>
|
||||
<p class="faellig__titel">Follow-up fällig</p>
|
||||
<ul class="faellig__liste" id="faellig-liste"></ul>
|
||||
</section>
|
||||
|
||||
<section class="neu" id="neu-block" hidden>
|
||||
<h2 class="neu__titel">Neuer Kontakt</h2>
|
||||
<form id="neu-form">
|
||||
<div class="neu__raster">
|
||||
<div class="feld feld--breit">
|
||||
<label class="feld-schild" for="n-name">Name</label>
|
||||
<input id="n-name" maxlength="120" required placeholder="Wie heißt die Person?" />
|
||||
</div>
|
||||
<div class="feld">
|
||||
<label class="feld-schild" for="n-plattform">Plattform</label>
|
||||
<input id="n-plattform" maxlength="60" placeholder="z. B. TikTok" />
|
||||
</div>
|
||||
<div class="feld">
|
||||
<label class="feld-schild" for="n-handle">Profil / Handle</label>
|
||||
<input id="n-handle" maxlength="120" placeholder="@…" />
|
||||
</div>
|
||||
<div class="feld">
|
||||
<label class="feld-schild" for="n-prio">Priorität</label>
|
||||
<select id="n-prio">
|
||||
<option value="hoch">Hoch</option>
|
||||
<option value="mittel" selected>Mittel</option>
|
||||
<option value="niedrig">Niedrig</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="feld" id="n-scout-block" hidden>
|
||||
<label class="feld-schild" for="n-scout">Scout</label>
|
||||
<select id="n-scout"><option value="">– niemandem zugeordnet –</option></select>
|
||||
</div>
|
||||
<div class="feld feld--breit">
|
||||
<label class="feld-schild" for="n-aktivitaet">Aktivität & LIVE-Fokus</label>
|
||||
<input id="n-aktivitaet" maxlength="3000" placeholder="Wie oft LIVE? Welche Zeiten? Welches Format?" />
|
||||
</div>
|
||||
<div class="feld feld--breit">
|
||||
<label class="feld-schild" for="n-potenzial">Potenzial & Stil</label>
|
||||
<input id="n-potenzial" maxlength="3000" placeholder="Was macht die Person besonders?" />
|
||||
</div>
|
||||
<div class="feld">
|
||||
<label class="feld-schild" for="n-followup">Nächster Follow-up</label>
|
||||
<input id="n-followup" type="date" />
|
||||
</div>
|
||||
</div>
|
||||
<p class="fehler" id="n-fehler" role="alert" aria-live="polite"></p>
|
||||
<div class="neu__knoepfe">
|
||||
<button type="submit" class="knopf knopf--klein" id="n-speichern">Aufnehmen</button>
|
||||
<button type="button" class="knopf-still" id="neu-zu">Abbrechen</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<div id="pipeline" aria-busy="true"><p class="leise">Pipeline wird geladen …</p></div>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- Zugangscode nach dem Onboarding: genau einmal sichtbar. -->
|
||||
<div class="schleier" id="code-schleier" hidden>
|
||||
<div class="code-fenster" role="dialog" aria-modal="true" aria-labelledby="code-titel">
|
||||
<p class="marke">Onboarding gestartet</p>
|
||||
<h2 class="code-fenster__titel" id="code-titel">Zugangscode</h2>
|
||||
<p class="code-fenster__text">
|
||||
Dieser Code wird <strong>genau einmal</strong> angezeigt und ist danach nicht mehr
|
||||
abrufbar. Gib ihn persönlich weiter, nicht über einen offenen Kanal.
|
||||
</p>
|
||||
<p class="code-fenster__code" id="code-wert">—</p>
|
||||
<div class="neu__knoepfe">
|
||||
<button type="button" class="knopf knopf--klein" id="code-kopieren">Kopieren</button>
|
||||
<button type="button" class="knopf-still" id="code-zu">Habe ich notiert</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="assets/js/kopf.js" defer></script>
|
||||
<script src="assets/js/scouting.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user