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
+56 -4
View File
@@ -55,6 +55,27 @@ export const NETZWERK = ["nein", "ja", "unbekannt"];
export const NETZWERK_NAME = {
nein: "frei", ja: "schon im Netzwerk", unbekannt: "noch nicht gefragt",
};
/* NORMAL ODER PREMIUM -- die beiden Arten, die TikTok kennt (06.10.2026).
Filipe: "es gibt bei tiktok nämlich diese zwei optionen von streamer."
GENAU ZWEI, und absichtlich kein "weiss nicht". Die Begruendung steht
bei der Spalte in workspace.js; kurz: Hier wird nicht eine fremde
Tatsache festgehalten, sondern eine eigene Absicht -- und deren
Normalfall ist "normal".
DIE REIHENFOLGE IST DIE DER SPALTEN auf der Seite: links normal,
rechts premium. Die Oberflaeche liest sie von hier, sie fuehrt keine
eigene Liste -- sonst stuende die Trennung an zwei Stellen und wuerde
an einer davon geaendert. */
export const ARTEN = ["normal", "premium"];
export const ART_NAME = { normal: "Normal", premium: "Premium" };
/* NULL ist "normal". Diese Umrechnung steht an EINER Stelle, weil sonst
jede Abfrage, jede Spaltenzuteilung und jede Zaehlung ihre eigene
Meinung dazu haette, wo ein Lead ohne Eintrag hingehoert. */
export const artVon = (wert) => (wert === "premium" ? "premium" : "normal");
const NAME_MAX = 120;
const TEXT_MAX = 3000;
@@ -120,6 +141,7 @@ const SPALTEN = `
l.aktivitaet, l.potenzial, l.notizen, l.letzte_nachricht, l.naechster_followup,
l.scout_id, l.erstellt, l.geaendert, l.uebergeben_am, l.creator_id,
l.follower, l.land, l.woher, l.kontaktweg, l.live_zeiten, l.netzwerk, l.absage_grund,
l.creator_art,
ps.name AS scout_name, pc.name AS creator_name`;
const VERBUND = `
@@ -143,7 +165,11 @@ scoutRouter.get("/workspace/api/leads", (req, res) => {
andere nicht -- zuletzt bei der Rollenwahl am 10.09. */
laender: LAENDER, land_name: LAND_NAME,
netzwerk: NETZWERK, netzwerk_name: NETZWERK_NAME,
arten: ARTEN, art_name: ART_NAME,
heute,
/* `creator_art` wird hier schon aufgeloest (NULL -> "normal"),
damit die Oberflaeche nicht ihre eigene Regel dafuer braucht --
sie waere die zweite und wuerde irgendwann die andere sagen. */
leads: db().prepare(`
SELECT ${SPALTEN} ${VERBUND}
WHERE ${wo}
@@ -153,7 +179,8 @@ scoutRouter.get("/workspace/api/leads", (req, res) => {
WHEN 'uebergeben' THEN 4 ELSE 5 END,
CASE l.prioritaet WHEN 'hoch' THEN 0 WHEN 'mittel' THEN 1 ELSE 2 END,
CASE WHEN l.naechster_followup IS NULL THEN 1 ELSE 0 END,
l.naechster_followup, l.id DESC`).all(...werte),
l.naechster_followup, l.id DESC`).all(...werte)
.map((l) => ({ ...l, creator_art: artVon(l.creator_art) })),
});
} catch (fehler) {
console.error("[workspace] Leads lesen:", fehler?.message);
@@ -216,6 +243,19 @@ function pruefe(körper, { neu }) {
else if (!NETZWERK.includes(w)) fehler.push("Unbekannte Angabe zum Netzwerk.");
else aus.netzwerk = w;
}
/* Normal oder Premium. Ein leerer Wert ist hier NICHT dasselbe wie bei
Land und Netzwerk: Dort heisst leer "nicht angegeben" und wird als
NULL gespeichert, hier gibt es keinen dritten Zustand -- leer faellt
auf "normal" zurueck, so wie es die Spalte ohnehin liest. Ein
erfundener Wert wird trotzdem abgelehnt und nicht stillschweigend
zu "normal" gemacht: Wer "premiumm" schickt, hat sich vertippt und
soll es erfahren, statt die Karte auf der falschen Seite zu finden. */
if (körper.creator_art !== undefined) {
const w = String(körper.creator_art ?? "").trim();
if (!w) aus.creator_art = "normal";
else if (!ARTEN.includes(w)) fehler.push("Unbekannte Creator-Art.");
else aus.creator_art = w;
}
if (körper.follower !== undefined) {
const roh = String(körper.follower ?? "").trim();
if (!roh) aus.follower = null;
@@ -270,8 +310,9 @@ scoutRouter.post("/workspace/api/leads", gleicheHerkunft, (req, res) => {
INSERT INTO leads
(name, plattform, handle, status, prioritaet, aktivitaet, potenzial,
notizen, letzte_nachricht, naechster_followup, scout_id, erstellt, erstellt_von,
follower, land, woher, kontaktweg, live_zeiten, netzwerk, absage_grund)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(
follower, land, woher, kontaktweg, live_zeiten, netzwerk, absage_grund,
creator_art)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(
aus.name, aus.plattform ?? null, aus.handle ?? null,
aus.status ?? "neu", aus.prioritaet ?? "mittel",
aus.aktivitaet ?? null, aus.potenzial ?? null, aus.notizen ?? null,
@@ -279,7 +320,7 @@ scoutRouter.post("/workspace/api/leads", gleicheHerkunft, (req, res) => {
scoutId, jetzt(), req.person.id,
aus.follower ?? null, aus.land ?? null, aus.woher ?? null,
aus.kontaktweg ?? null, aus.live_zeiten ?? null, aus.netzwerk ?? null,
aus.absage_grund ?? null);
aus.absage_grund ?? null, aus.creator_art ?? "normal");
protokolliere("lead_angelegt", {
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
@@ -382,6 +423,17 @@ scoutRouter.post("/workspace/api/leads/:id/onboarding", gleicheHerkunft, (req, r
db().prepare("UPDATE leads SET creator_id = ?, geaendert = ? WHERE id = ?")
.run(neu.id, jetzt(), id);
/* DIE ART GEHT MIT (06.10.2026).
Sonst endet die Angabe genau in dem Moment, in dem sie zum ersten
Mal vertraglich zaehlt: Der Scout haelt fest, dass jemand als
Premium geholt werden soll, das Onboarding legt die Person an --
und am Creator steht es nirgends mehr. Ab hier ist es eine Angabe
an der Person und laeuft dem Lead nicht mehr hinterher; geaendert
wird sie danach in der Personenverwaltung. */
db().prepare("UPDATE personen SET creator_art = ? WHERE id = ?")
.run(artVon(lead.creator_art), neu.id);
/* Die Angaben des Scouts wandern ins Creator-Profil -- sonst müsste
das Management sie abtippen, obwohl sie längst da sind. */
const handles = [lead.plattform, lead.handle].filter(Boolean).join(" · ") || null;