Scouting: links die normalen Creator, rechts die Premium

Filipe: "ich will dass es da in der mitte eine trennung gibt. links
sollen normale creator sein und rechts premium creator. es gibt bei
tiktok naemlich diese zwei optionen von streamer ... auch dass wenn man
die eintraegt soll man aussuchen koennen."

Jede Stufe der Pipeline hat jetzt zwei Spalten mit einer Trennlinie
dazwischen. Die Trennung sitzt IN der Stufe, nicht einmal ueber der
ganzen Seite: Zwei komplette Pipelines nebeneinander haetten jede
Stufenueberschrift verdoppelt und die beiden Seiten waeren nie auf
gleicher Hoehe gewesen.

GENAU ZWEI WERTE, kein "weiss nicht". Anders als bei `netzwerk`, wo
"noch nicht gefragt" eine eigene gueltige Antwort ist: Dort wird eine
fremde Tatsache festgehalten, hier eine eigene Absicht -- und deren
Normalfall ist "normal". NULL wird als "normal" gelesen, der eine
vorhandene Lead steht damit links, ohne dass ihm etwas unterstellt wird.

DIE ART UEBERLEBT DIE UEBERGABE. Beim Creator-Onboarding wandert sie auf
die Person und ist in der Personenverwaltung aenderbar. Ohne das endet
die Angabe genau dort, wo sie zum ersten Mal vertraglich zaehlt.

Die Schwelle misst den STUFENKASTEN (@container), nicht das Fenster --
der Fehler vom 06.09.2026 im selben Haus. Auf dem Handy liegen die
Spalten untereinander, die senkrechte Linie faellt weg.

ZWEI FUNDE BEIM BAUEN, beide von Hauswachen:
 * `pruef-fingermass` fand, dass meine neue Handy-Messung mit
   `setViewportSize` statt eigenem Kontext lief -- ohne `hasTouch`
   greift keine einzige Regel aus `@media (pointer: coarse)`.
 * Die Klassen hiessen zuerst `.spalte__*` -- die gehoeren `aufgaben.css`,
   und scouting.html laedt die VOR scouting.css. `margin-left: auto` aus
   dem Aufgabenbrett zog die Zahl 560 px von ihrem Wort weg, ohne dass
   eine Pruefung rot wurde. Jetzt `.art-spalte__*`, und der Abstand wird
   gemessen.

pruef-scouting-felder: 28 -> 64 Pruefungen, gruen. Zwei Gegenproben
gefahren (Trennung deaktiviert, Spaltenreihenfolge gedreht) -- beide
schlugen an. Dazu gruen: haus-trennung, css-klassen, personen-liste,
personen-kachel, hand-personen, schranke, formulare, lesbarkeit,
agentur, deutsche-texte, tippziele, leerzustand, fingermass.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-10-06 23:33:12 +02:00
co-authored by Claude Opus 5
parent c4bdc1c4c2
commit f18ab32f8c
55 changed files with 1588 additions and 701 deletions
+13
View File
@@ -142,6 +142,19 @@
justify-content: flex-start; margin-top: auto;
}
/* NORMAL ODER PREMIUM (06.10.2026).
Dasselbe Gold wie in der Scout-Pipeline (--k-gold) -- die Marke heisst
dort und hier dasselbe und soll deshalb auch gleich aussehen. Zwei
Toene fuer eine Aussage waeren zwei Aussagen.
Nur Premium bekommt eine Marke. "Normal" auf jeder zweiten Kachel
waere ein Wort, das nie etwas Neues sagt. */
.marke-rolle[data-art="premium"] {
background: color-mix(in srgb, var(--k-gold) 16%, transparent);
color: var(--k-gold);
}
/* Gesperrt ist jetzt eine Marke in der Warnfarbe, kein graues Wort in
einer Reihe grauer Woerter. */
.marke-rolle[data-rolle="gesperrt"] {
+109
View File
@@ -322,3 +322,112 @@
.kk__fakt, .knopf-still--bearbeiten { border: 1px solid CanvasText; }
.marke-punkt[data-warn="ja"] { border: 2px solid CanvasText; }
}
/* =====================================================================
LINKS NORMAL, RECHTS PREMIUM (06.10.2026)
Filipe: "ich will dass es da in der mitte eine trennung gibt. links
sollen normale creator sein und rechts premium creator. es gibt bei
tiktok nämlich diese zwei optionen von streamer."
DIE SCHWELLE WIRD AM STUFENKASTEN GEMESSEN, NICHT AM FENSTER
(@container statt @media). Das ist der Unterschied, der am 06.09.2026
im Creator Workspace zwei Fehler gekostet hat: Eine Zahl, die das
FENSTER misst, weiß nichts von Rändern, Seitenleisten oder davon,
dass der Inhalt bei 1240px aufhört. Der Kasten misst sich selbst --
er bricht genau dann um, wenn ER zu schmal wird, und bleibt richtig,
auch wenn sich an der Seite drumherum etwas ändert.
DIE 680 STEHT GENAU EINMAL und sie zählt etwas Bestimmtes:
zwei Kartenspalten von je 320px (schmaler wird eine Karte mit ihren
zwei Knöpfen unbedienbar) plus 40px Trennabstand. Wer die Karte
breiter macht, ändert sie hier -- und nirgendwo sonst.
WARUM DIE KLASSEN „art-spalte" HEISSEN UND NICHT „spalte":
`aufgaben.css` besitzt `.spalte`, `.spalte__kopf`, `.spalte__zahl` und
`.spalte__leer` -- das Aufgabenbrett. Und `scouting.html` lädt
`aufgaben.css` VOR dieser Datei. Der erste Entwurf hieß hier
`.spalte__zahl`, und prompt zog `margin-left: auto` aus dem
Aufgabenbrett die Zahl ans rechte Ende der Spalte, 560 px von ihrem
Wort entfernt. Keine Prüfung wurde dabei rot: Die Klasse war ja da,
sie gehörte nur jemand anderem. Gesehen habe ich es erst im
Bildschirmfoto -- deshalb misst `pruef-scouting-felder` jetzt den
Abstand zwischen Wort und Zahl.
===================================================================== */
.stufe { container-type: inline-size; }
/* Untereinander ist der Grundzustand, nicht der Notfall: So sieht es
auf dem Handy aus, und dort wird diese Seite am häufigsten benutzt. */
.stufe__arten { display: grid; gap: 22px; }
.art-spalte__kopf {
display: flex; align-items: center; gap: 8px;
margin-bottom: 9px;
}
.art-spalte__titel {
margin: 0;
/* .76rem = 12,2px. Unter 11,5px gilt Text im Großcheck als zu klein
zum Lesen -- und das ist er auch. */
font-size: .76rem; font-weight: 700;
letter-spacing: .09em; text-transform: uppercase;
color: var(--text-still);
}
.art-spalte__zahl {
min-width: 20px; padding: 1px 7px; text-align: center;
font-size: .72rem; font-weight: 700; color: var(--text-still);
background: rgba(255, 255, 255, .06);
border-radius: 999px;
}
/* Premium trägt das Gold des Hauses (--k-gold). Kein neuer Farbwert:
Das Haus hat einen Ton für "das hier ist etwas Besonderes", und zwei
verschiedene Goldtöne nebeneinander wären nur zwei. */
.art-spalte[data-art="premium"] .art-spalte__titel { color: var(--k-gold); }
.art-spalte[data-art="premium"] .art-spalte__zahl {
color: var(--k-gold);
background: color-mix(in srgb, var(--k-gold) 13%, transparent);
}
/* Eine leere Spalte verschwindet NICHT. Sonst hätte eine Stufe mal eine
und mal zwei Spalten, und man wüsste nicht, ob dort niemand steht
oder ob die Trennung an dieser Stelle nicht gilt. */
.art-spalte__leer {
margin: 0; padding: 12px 2px;
font-size: .82rem; color: var(--text-still);
}
@container (min-width: 680px) {
.stufe__arten {
grid-template-columns: 1fr 1fr;
gap: 0;
}
/* DIE TRENNUNG IN DER MITTE. Eine Linie am linken Rand der rechten
Spalte -- sie läuft dadurch genau so weit, wie die längere der
beiden Spalten reicht, und hört von selbst auf. Ein eigenes Element
dafür müsste in der Höhe nachgeführt werden und wäre bei jeder
aufgeklappten Karte zu kurz. */
.art-spalte[data-art="normal"] { padding-right: 20px; }
.art-spalte[data-art="premium"] {
padding-left: 20px;
/* --rand-hell (0,30) statt --rand (0,16). Im Bildschirmfoto vom
06.10.2026 verschwand die dünnere Linie über dem Hintergrundbild
fast ganz -- und genau sie war das Gewünschte. 0,30 bleibt eine
ruhige Linie und kein Strich. */
border-left: 1px solid var(--rand-hell);
}
}
/* ---- Die Marke auf der Karte ---------------------------------------
Nur Premium bekommt eine. "Normal" auf jeder zweiten Karte wäre ein
Wort, das nie etwas Neues sagt -- dieselbe Regel wie bei "Schon im
Netzwerk", wo "frei" ebenfalls stumm bleibt. */
.marke-punkt[data-art="premium"] {
color: var(--k-gold);
border-color: color-mix(in srgb, var(--k-gold) 42%, transparent);
background: color-mix(in srgb, var(--k-gold) 12%, transparent);
}
@media (forced-colors: active) {
.art-spalte[data-art="premium"] { border-left: 1px solid CanvasText; }
.marke-punkt[data-art="premium"] { border: 2px solid CanvasText; }
}
+66 -1
View File
@@ -426,6 +426,12 @@ Der Code gilt einmal fuer die `
/* ---------- Liste ----------------------------------------------------- */
let betreuerListe = [];
/* Normal und Premium kommen vom Server (siehe die Personenliste).
Die Vorgabe hier ist die Rueckfallebene fuer den Fall, dass ein
alter Serverstand die Liste noch nicht mitschickt -- dann fehlt
die Auswahl nicht, sie ist nur nicht nachgefuehrt. */
let artenListe = ['normal', 'premium'];
let artName = { normal: 'Normal', premium: 'Premium' };
/** Ein oder zwei Buchstaben aus einem Namen.
*
@@ -562,6 +568,22 @@ Der Code gilt einmal fuer die `
anderen grauen Woertern -- auf einer Kachel, die ohnehin blasser
ist, hat es niemand gesehen. Jetzt ist es eine Marke in der
Warnfarbe. */
/* PREMIUM STEHT NEBEN DER ROLLE (06.10.2026).
"Creator" allein sagt nicht mehr alles, seit es die beiden Arten
gibt, die TikTok kennt. Die Marke steht deshalb direkt neben der
Rolle und nicht im Zahlenband -- sie beschreibt, WER jemand ist,
nicht wie viel er tut.
Nur Premium bekommt eine Marke, genau wie in der Pipeline:
"Normal" auf jeder zweiten Kachel wiederholte nur, was ohnehin
der Normalfall ist. */
if (p.rolle === 'creator' && p.creator_art === 'premium') {
const a = el('span', 'marke-rolle', 'Premium');
a.dataset.art = 'premium';
a.title = 'Premium-Creator – die zweite der beiden Arten, die TikTok kennt.';
marken.append(a);
}
if (!p.aktiv) {
const g = el('span', 'marke-rolle', 'gesperrt');
g.dataset.rolle = 'gesperrt';
@@ -637,6 +659,44 @@ Der Code gilt einmal fuer die `
links.append(el('div', 'person__bezug', `gehört zu ${p.manager_name}`));
}
/* NORMAL ODER PREMIUM — änderbar (06.10.2026).
Beim Creator-Onboarding kommt die Art aus der Scout-Pipeline mit.
Danach hängt sie an der Person, und hier wird sie geändert: Ein
Vertrag kann wechseln, und ein Lead, aus dem die Angabe einmal
stammte, kann längst gelöscht sein.
Dieselbe Bauart wie „Betreut von" direkt darunter — ein Auswahlfeld,
das beim Umstellen sofort speichert. Kein eigener Speichern-Knopf:
Bei einer Auswahl mit zwei Werten wäre er ein zweiter Klick, den
man vergisst, und dann steht etwas anderes da, als man sieht. */
if (p.rolle === 'creator') {
const a = el('div', 'betreuung');
const as = el('label', 'betreuung__schild', 'Art');
as.htmlFor = 'creator-art-' + p.id;
const aw = el('select', 'betreuung__wahl');
aw.id = 'creator-art-' + p.id;
for (const w of artenListe) aw.append(new Option(artName[w] || w, w));
aw.value = p.creator_art === 'premium' ? 'premium' : 'normal';
aw.addEventListener('change', async () => {
aw.disabled = true; melde('');
try {
const antwort = await hole(`/workspace/api/verwaltung/personen/${p.id}/creator-art`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ creator_art: aw.value }),
});
if (!antwort.ok) {
melde(sagWas((await antwort.json().catch(() => ({}))).fehler, 'Ging nicht.'));
return;
}
await laden();
} catch { /* umgeleitet */ } finally { aw.disabled = false; }
});
a.append(as, aw);
links.append(a);
}
/* Zuständigkeit. Steht nur bei Creator und nur dann, wenn es
überhaupt einen Scout gibt, dem man sie geben könnte. */
if (p.rolle === 'creator' && betreuerListe.length) {
@@ -1257,8 +1317,13 @@ Der Code gilt einmal fuer die `
try {
const a = await hole('/workspace/api/verwaltung/personen');
if (!a.ok) { melde('Personen konnten nicht geladen werden.'); return; }
const { personen, betreuer, zusatzrollen } = await a.json();
const { personen, betreuer, zusatzrollen, arten, art_name } = await a.json();
betreuerListe = betreuer || [];
/* Nur uebernehmen, wenn wirklich etwas kommt: `|| []` haette hier
eine leere Auswahl ergeben -- ein Feld ohne einen einzigen
Eintrag sieht aus wie ein Fehler und ist einer. */
if (Array.isArray(arten) && arten.length) artenListe = arten;
if (art_name) artName = art_name;
zusatzAbschnitte = Array.isArray(zusatzrollen) ? zusatzrollen : [];
rollenwahlErgaenzen(zusatzrollen);
alle = personen;
+122 -7
View File
@@ -48,6 +48,15 @@
Oberflaeche fuehrt keine eigenen. Leere Vorgabe, damit ein
Ausfall der Abfrage kein kaputtes Formular ergibt. */
let laenderListe = [], landName = {}, netzwerkListe = [], netzwerkName = {};
/* Normal und Premium -- die beiden Arten, die TikTok kennt.
DIE RUECKFALLLISTE IST KEIN SCHMUCK: Sie steht hier, damit die
Trennung auch dann zwei Spalten hat, wenn ein alter Server die
Listen noch nicht mitschickt. Ohne sie waere die Seite in genau dem
Moment leer, in dem jemand sie zum ersten Mal nach einem Deploy
aufmacht -- der Server neu, die Datei aus dem Zwischenspeicher alt,
oder umgekehrt. */
let artenListe = ['normal', 'premium'], artName = { normal: 'Normal', premium: 'Premium' };
let heute = '';
let daten = [];
let offen = new Set();
@@ -111,7 +120,13 @@
/* Kopf */
const kopf = el('div', 'kk__kopf');
const links = el('div');
links.append(el('h3', 'kk__name', l.name));
/* h4, NICHT h3 (06.10.2026). Ueber der Karte stehen jetzt drei
Ebenen: "Pipeline" (h1), die Stufe (h2) und die Spalte Normal /
Premium (h3). Bliebe der Name ein h3, saesse er auf derselben
Ebene wie die Spalte, in der er liegt -- wer sich per
Ueberschriften durch die Seite bewegt, bekaeme die Trennung nicht
mehr zu hoeren. */
links.append(el('h4', '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);
@@ -134,6 +149,23 @@
n.title = 'Noch nicht geklärt, ob die Person schon bei einer Agentur ist.';
marken.append(n);
}
/* PREMIUM TRAEGT EIN ZEICHEN, NORMAL NICHT (06.10.2026).
Dieselbe Regel wie bei "Schon im Netzwerk": Nur der Fall, der vom
Normalfall abweicht, bekommt eine Marke. Eine Marke "Normal" auf
jeder zweiten Karte waere ein Wort, das nie etwas Neues sagt.
WARUM UEBERHAUPT, WO DOCH DIE SPALTE ES SCHON SAGT: Die beiden
Spalten werden verschieden lang. Steht links eine lange Liste und
rechts eine kurze, ist eine Karte weit unten vom Kopf ihrer Spalte
durch einen halben Bildschirm getrennt -- und auf dem Handy liegen
die Spalten ohnehin untereinander. */
if (l.creator_art === 'premium') {
const a = el('span', 'marke-punkt', artName.premium || 'Premium');
a.dataset.art = 'premium';
a.title = 'Premium-Creator – die zweite der beiden Arten, die TikTok kennt.';
marken.append(a);
}
const prio = el('span', 'marke-punkt', l.prioritaet === 'hoch' ? 'Hoch'
: l.prioritaet === 'niedrig' ? 'Niedrig' : 'Mittel');
prio.dataset.prio = l.prioritaet;
@@ -240,6 +272,16 @@
const o = document.createElement('option'); o.value = w; o.textContent = t; f.append(o);
}
});
/* Die Art laesst sich auch nachtraeglich aendern -- ein Lead rutscht
dabei in die andere Spalte. Genau das war der Mangel vom
11.09.2026 ("man kan da nichts machen"): ein Feld, das man beim
Anlegen setzt und danach nie wieder. */
const fArt = mach('Art', 'select', l.creator_art || 'normal', (f) => {
for (const w of artenListe) {
const o = document.createElement('option');
o.value = w; o.textContent = artName[w] || w; 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);
@@ -310,6 +352,7 @@
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,
creator_art: fArt.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,
@@ -475,13 +518,58 @@
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);
/* ====== LINKS NORMAL, RECHTS PREMIUM (06.10.2026) ==============
Filipe: "ich will dass es da in der mitte eine trennung gibt.
links sollen normale creator sein und rechts premium creator.
es gibt bei tiktok nämlich diese zwei optionen von streamer."
DIE TRENNUNG SITZT IN JEDER STUFE, nicht einmal ueber der
ganzen Seite. Zwei komplette Pipelines nebeneinander haetten
jede Stufenueberschrift zweimal gebraucht, und die beiden
Seiten waeren nie auf gleicher Hoehe gewesen -- "Angesprochen"
links auf Hoehe von "Interessiert" rechts. So bleibt die Stufe
die Ueberschrift und die Art die Spalte darunter.
DIE REIHENFOLGE KOMMT VOM SERVER (artenListe) und ist dort
"normal, premium" -- links, rechts. Sie steht nicht zweimal da;
waere sie hier noch einmal hingeschrieben, koennte eine der
beiden Stellen gedreht werden und die Spalten wuerden
vertauscht, ohne dass irgendetwas kaputtginge.
EINE LEERE SPALTE BLEIBT STEHEN und sagt "noch niemand". Wer
sie wegliesse, haette eine Stufe, die ploetzlich eine Spalte
hat statt zwei -- und man wuesste nicht, ob da niemand ist oder
ob die Trennung an dieser Stelle nicht gilt. Die Zahl im Kopf
beantwortet es ohne Nachzaehlen. */
const spalten = el('div', 'stufe__arten');
for (const art of artenListe) {
const inDieser = drin.filter((l) => (l.creator_art || 'normal') === art);
const sp = el('section', 'art-spalte');
sp.dataset.art = art;
const spKopf = el('div', 'art-spalte__kopf');
const spTitel = el('h3', 'art-spalte__titel', artName[art] || art);
/* Die Ueberschrift benennt den Bereich fuer die Sprachausgabe --
sonst liest sie zwei Kartenstapel vor, ohne zu sagen, welcher
welcher ist. */
spTitel.id = `art-spalte-${wert}-${art}`;
sp.setAttribute('aria-labelledby', spTitel.id);
spKopf.append(spTitel, el('span', 'art-spalte__zahl', String(inDieser.length)));
sp.append(spKopf);
const box = el('div', 'stufe__karten');
if (!inDieser.length) box.append(el('p', 'art-spalte__leer', 'noch niemand'));
for (const l of inDieser) {
const k = karte(l);
k.id = 'lead-' + l.id;
box.append(k);
}
sp.append(box);
spalten.append(sp);
}
gruppe.append(box);
gruppe.append(spalten);
ziel.append(gruppe);
}
}
@@ -494,12 +582,38 @@
stufen = d.stufen; heute = d.heute; daten = d.leads;
laenderListe = d.laender || []; landName = d.land_name || {};
netzwerkListe = d.netzwerk || []; netzwerkName = d.netzwerk_name || {};
/* Nur uebernehmen, wenn wirklich etwas kommt -- sonst stuende hier
eine leere Liste und die Seite haette null Spalten statt zwei.
`|| []` waere an dieser Stelle genau der stille Ausfall, den die
Rueckfallliste oben verhindern soll. */
if (Array.isArray(d.arten) && d.arten.length) artenListe = d.arten;
if (d.art_name) artName = d.art_name;
artwahlFuellen();
zeichne();
} catch { /* umgeleitet */ }
}
/* ---------- Neuer Kontakt ------------------------------------------------ */
/* Die Auswahl im Anlege-Formular wird aus derselben Liste gefuellt wie
die Spalten. In scouting.html stehen Normal und Premium zwar schon
fest drin -- das ist die Vorgabe, damit das Feld auch vor der ersten
Antwort bedienbar ist. Sobald der Server geantwortet hat, gilt
seine Liste, und zwar auch dann, wenn dort eines Tages etwas anderes
steht. Die bereits getroffene Wahl bleibt erhalten. */
function artwahlFuellen() {
const f = $('n-art');
if (!f) return;
const vorher = f.value;
f.textContent = '';
for (const w of artenListe) {
const o = document.createElement('option');
o.value = w; o.textContent = artName[w] || w;
f.append(o);
}
f.value = artenListe.includes(vorher) ? vorher : (artenListe[0] || '');
}
$('neu-auf').addEventListener('click', () => {
$('neu-block').hidden = false;
$('neu-auf').hidden = true;
@@ -523,6 +637,7 @@
body: JSON.stringify({
name: $('n-name').value, plattform: $('n-plattform').value,
handle: $('n-handle').value, prioritaet: $('n-prio').value,
creator_art: $('n-art').value,
aktivitaet: $('n-aktivitaet').value, potenzial: $('n-potenzial').value,
naechster_followup: $('n-followup').value,
scout_id: $('n-scout').value || null,