Manager sehen nur noch ihre zugeteilten Scouts -- und deren Creator
Wunsch vom 01.09.2026: "er soll nur die Scouts sehen, die ihm zugeteilt sind." Dazu entschieden: Ein Manager sieht dann AUCH die Creator dieser Scouts, und zuteilen darf NUR DogFather. WAS VORHER WAR. Ein Manager sah die gesamte Scout-Pipeline -- alle Leads aller Scouts, dazu dieselben in der Suche und in "Was ist dran". Das war die letzte Stelle, an der "nur DogFather sieht alles" noch nicht galt. EINE EIGENE TABELLE, KEIN ZWEITER EINTRAG IN `betreuung`. Dort heisst die Spalte creator_id, und der ganze uebrige Code liest sie als "das ist ein Creator". Ein Scout darin waere technisch moeglich und fachlich eine Luege gewesen: betreuteIds() gaebe Scout-Nummern zurueck, die anderswo als Creator behandelt wuerden. Solche Abkuerzungen raechen sich genau dann, wenn niemand mehr weiss, dass sie getroffen wurden. DIE KETTE steht an EINER Stelle (betreuteIds in workspace.js): Manager -> seine Scouts -> deren Creator. Ohne sie muesste jeder Creator einem Manager einzeln zugewiesen werden, und beim ersten vergessenen faende er ein Loch in seiner Uebersicht, ohne zu merken, dass es eines ist. Dieselbe Regel gilt fuer Leads in Pipeline, Suche und Hinweisen -- aus einer Quelle (pipelineIds), nicht dreimal abgeschrieben. ZUTEILEN DARF NUR DOGFATHER, und das ist keine Foermlichkeit: Diese Zuteilung ERWEITERT die Sicht eines Managers. Duerfte er sie selbst setzen, koennte er sich seine eigene Sichtbarkeit vergeben -- eine Grenze, die der Begrenzte selbst verschieben kann, ist keine. Deshalb istDogFather und ausdruecklich NICHT istLeitung. In der Personenliste steht bei jedem Scout "Gehoert zu", sichtbar nur fuer DogFather. Der Leerwert heisst "— direkt bei DogFather —" und nicht "— niemand —": Ein Scout ohne Manager ist nicht unbetreut, er haengt an DogFather. Das ist ein Zustand, kein Mangel. NEBENBEI KORRIGIERT: In der Zustaendigkeits-Route stand als Kommentar noch "ein Eintrag auf DogFather oder Manager aendert an den Rechten nichts, die Leitung sieht ohnehin jeden Creator". Das gilt seit heute nicht mehr -- fuer einen Manager entscheidet dieser Eintrag jetzt sehr wohl ueber die Sichtbarkeit. Ein falscher Kommentar ist schlimmer als keiner: Er wird geglaubt. DIE PRUEFUNG STELLT DEN MISSBRAUCH AN DEN ANFANG. Diese Aenderung erweitert Sichtbarkeit -- alles andere heute hat sie eingeschraenkt. Ein Fehler dort zeigt jemandem zu wenig und faellt auf; ein Fehler hier zeigt zu viel und faellt niemandem auf. Geprueft wird deshalb: Manager und Scout duerfen nicht zuteilen (404, und es wird auch wirklich nichts geschrieben), Scout an Scout und Scout an DogFather werden abgewiesen, ein Creator laesst sich nicht zuteilen. Danach die Kette in beide Richtungen, das Zuruecknehmen, und dass ein Scout zu genau einem Manager gehoert. Ein lehrreicher Fehlschlag in der Pruefung selbst: Sie meldete, die Auswahl fehle in der Oberflaeche. Sie fehlte nicht -- die Personenliste ist nach Rollen ZUGEKLAPPT, und die Pruefung hatte nicht aufgeklappt. Sah aus wie ein Produktfehler, war einer der Pruefung. Sie klappt jetzt auf und stellt vorher fest, dass wirklich alle sieben Zeilen dastehen. Gesamtlauf: 30 von 30 Dateien, 1210 von 1210 Einzelpunkten. Co-Authored-By: Claude Opus 5 <[email protected]>
|
Before Width: | Height: | Size: 984 KiB After Width: | Height: | Size: 984 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 2.6 MiB |
|
Before Width: | Height: | Size: 921 KiB After Width: | Height: | Size: 921 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 757 KiB After Width: | Height: | Size: 761 KiB |
|
Before Width: | Height: | Size: 890 KiB After Width: | Height: | Size: 890 KiB |
|
Before Width: | Height: | Size: 858 KiB After Width: | Height: | Size: 859 KiB |
|
After Width: | Height: | Size: 939 KiB |
|
Before Width: | Height: | Size: 1013 KiB After Width: | Height: | Size: 1014 KiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 835 KiB After Width: | Height: | Size: 816 KiB |
|
Before Width: | Height: | Size: 924 KiB After Width: | Height: | Size: 924 KiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 246 KiB After Width: | Height: | Size: 246 KiB |
|
Before Width: | Height: | Size: 324 KiB After Width: | Height: | Size: 324 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
@@ -0,0 +1,335 @@
|
||||
/* Prueft die Zuteilung Manager -> Scouts.
|
||||
|
||||
Wunsch vom 01.09.2026: "er soll nur die Scouts sehen, die ihm
|
||||
zugeteilt sind." Dazu entschieden: Ein Manager sieht dann AUCH die
|
||||
Creator dieser Scouts ("ja, alles seiner Scouts"), und zuteilen darf
|
||||
NUR DogFather.
|
||||
|
||||
WARUM DAS DIE HEIKELSTE AENDERUNG DES TAGES IST.
|
||||
|
||||
Diese Zuteilung ERWEITERT Sichtbarkeit. Alles andere heute hat sie
|
||||
eingeschraenkt -- ein Fehler dort zeigt jemandem zu wenig, und das
|
||||
faellt auf. Ein Fehler HIER zeigt jemandem zu viel, und das faellt
|
||||
niemandem auf. Deshalb steht der Missbrauch am Anfang:
|
||||
|
||||
Darf ein Manager sich selbst Scouts zuteilen? -> muss 404 sein
|
||||
Darf ein Scout das? -> muss 404 sein
|
||||
Kann man einen Scout an einen Scout haengen? -> muss 400 sein
|
||||
Kann man einen Creator zuteilen? -> muss 404 sein
|
||||
|
||||
Und danach die Kette in beide Richtungen: Was der Manager sehen SOLL,
|
||||
sieht er -- und was er nicht sehen soll, sieht er nicht. */
|
||||
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
const ordner = mkdtempSync(join(tmpdir(), "ws-zut-"));
|
||||
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
||||
process.env.PORT = "4217";
|
||||
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
||||
process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1";
|
||||
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
|
||||
|
||||
const express = (await import("express")).default;
|
||||
const ec = express.response.cookie;
|
||||
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
|
||||
await import("./index.js");
|
||||
await new Promise((r) => setTimeout(r, 700));
|
||||
const BASIS = "http://127.0.0.1:4217";
|
||||
setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 300_000).unref?.();
|
||||
|
||||
let fehler = 0;
|
||||
const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
||||
|
||||
await fetch(BASIS + "/workspace/api/anmelden", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {});
|
||||
|
||||
const { DatabaseSync } = await import("node:sqlite");
|
||||
const { scryptSync, randomBytes } = await import("node:crypto");
|
||||
for (let i = 0; i < 60; i++) {
|
||||
try {
|
||||
const p = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); }
|
||||
} catch { await new Promise((r) => setTimeout(r, 250)); }
|
||||
}
|
||||
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
const jetzt = new Date().toISOString();
|
||||
const heute = jetzt.slice(0, 10);
|
||||
function anlegen(name, rolle, code) {
|
||||
const salt = randomBytes(16).toString("hex");
|
||||
const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
|
||||
d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)")
|
||||
.run(name, rolle, hash, salt, 32768, jetzt);
|
||||
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||||
}
|
||||
const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
||||
const idMara = anlegen("Mara", "manager", "CODE-MANA-0001");
|
||||
const idNina = anlegen("Nina", "manager", "CODE-MANB-0001");
|
||||
const idPat = anlegen("Patrick", "scout", "CODE-SCOU-0001");
|
||||
const idBen = anlegen("Ben", "scout", "CODE-SCOB-0001");
|
||||
const idTili = anlegen("Tili", "creator", "CODE-ANNA-0001");
|
||||
const idLuna = anlegen("Luna", "creator", "CODE-LUNA-0001");
|
||||
|
||||
/* Patrick betreut Tili, Ben betreut Luna. Noch KEINE Scout-Zuteilung --
|
||||
die wird gleich ueber die Schnittstelle gesetzt, damit auch der Weg
|
||||
dorthin geprueft ist und nicht nur die Datenbank. */
|
||||
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idTili, idPat, jetzt);
|
||||
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idLuna, idBen, jetzt);
|
||||
|
||||
const lead = (name, scout) =>
|
||||
d.prepare(`INSERT INTO leads (name, plattform, handle, status, prioritaet, scout_id, erstellt)
|
||||
VALUES (?,?,?,?,?,?,?)`)
|
||||
.run(name, "tiktok", "@" + name.toLowerCase(), "neu", "mittel", scout, jetzt);
|
||||
lead("PATRICK-LEAD", idPat);
|
||||
lead("BEN-LEAD", idBen);
|
||||
|
||||
const aufgabe = (titel, creator, verant) =>
|
||||
d.prepare(`INSERT INTO aufgaben (titel, beschreibung, status, prioritaet,
|
||||
creator_id, verantwortlich_id, erstellt, erstellt_von) VALUES (?,?,?,?,?,?,?,?)`)
|
||||
.run(titel, "", "offen", "mittel", creator, verant, jetzt, idDogi);
|
||||
aufgabe("TILI-AUFGABE", idTili, idPat);
|
||||
aufgabe("LUNA-AUFGABE", idLuna, idBen);
|
||||
d.close();
|
||||
|
||||
async function anmelden(rolle, code) {
|
||||
const a = await fetch(BASIS + "/workspace/api/anmelden", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ rolle, code }),
|
||||
});
|
||||
return (a.headers.getSetCookie?.() || []).map((x) => x.split(";")[0]).join("; ");
|
||||
}
|
||||
const setzen = (scoutId, managerId, keks) =>
|
||||
fetch(`${BASIS}/workspace/api/verwaltung/scout-zuteilung/${scoutId}`, {
|
||||
method: "PUT",
|
||||
headers: { cookie: keks, "Content-Type": "application/json", origin: BASIS },
|
||||
body: JSON.stringify({ manager_id: managerId }),
|
||||
});
|
||||
const holen = async (pfad, keks) => {
|
||||
const a = await fetch(BASIS + pfad, { headers: { cookie: keks } });
|
||||
return { status: a.status, daten: a.ok ? await a.json() : null };
|
||||
};
|
||||
|
||||
const kDogi = await anmelden("admin", "CODE-DOGI-0001");
|
||||
const kMara = await anmelden("manager", "CODE-MANA-0001");
|
||||
const kNina = await anmelden("manager", "CODE-MANB-0001");
|
||||
const kPat = await anmelden("scout", "CODE-SCOU-0001");
|
||||
|
||||
/* ================================================================
|
||||
1. ZUERST DER MISSBRAUCH
|
||||
================================================================ */
|
||||
console.log("\n=== Nur DogFather darf zuteilen");
|
||||
|
||||
{
|
||||
const a = await setzen(idPat, idMara, kMara);
|
||||
ok(a.status === 404,
|
||||
`ein Manager kann sich NICHT selbst einen Scout zuteilen (${a.status}, 404 erwartet)`);
|
||||
/* 404 und nicht 403: Wer es nicht darf, soll nicht einmal erfahren,
|
||||
dass es diesen Weg gibt. */
|
||||
const b = await setzen(idPat, idMara, kPat);
|
||||
ok(b.status === 404, `ein Scout auch nicht (${b.status})`);
|
||||
|
||||
/* Und wirklich nichts passiert? Eine abgewiesene Anfrage, die trotzdem
|
||||
schreibt, waere der schlimmste Fall -- deshalb wird nachgesehen. */
|
||||
const d2 = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
const n = d2.prepare("SELECT COUNT(*) AS n FROM scout_zuteilung").get().n;
|
||||
d2.close();
|
||||
ok(n === 0, `und es wurde auch nichts geschrieben (${n} Zuteilungen)`);
|
||||
}
|
||||
|
||||
console.log("\n=== Unsinnige Zuteilungen");
|
||||
{
|
||||
const a = await setzen(idPat, idBen, kDogi);
|
||||
ok(a.status === 400, `Scout an Scout geht nicht (${a.status})`);
|
||||
const b = await setzen(idPat, idDogi, kDogi);
|
||||
ok(b.status === 400,
|
||||
`Scout an DogFather geht nicht (${b.status}) – er sieht ohnehin alles, `
|
||||
+ "und ein Eintrag ohne Wirkung wird spaeter fuer eine Wirkung gehalten");
|
||||
const c = await setzen(idTili, idMara, kDogi);
|
||||
ok(c.status === 404, `einen Creator kann man nicht zuteilen (${c.status})`);
|
||||
const e = await setzen(999999, idMara, kDogi);
|
||||
ok(e.status === 404, `eine erfundene Nummer ebenfalls nicht (${e.status})`);
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
2. DIE KETTE: Manager -> Scout -> dessen Creator
|
||||
================================================================ */
|
||||
console.log("\n=== Vorher: Mara sieht nichts von Patrick");
|
||||
{
|
||||
const l = await holen("/workspace/api/leads", kMara);
|
||||
ok((l.daten?.leads || []).length === 0,
|
||||
`ohne Zuteilung keine fremden Leads (${(l.daten?.leads || []).length})`);
|
||||
const a = await holen("/workspace/api/aufgaben", kMara);
|
||||
ok((a.daten?.aufgaben || []).length === 0,
|
||||
`und keine fremden Aufgaben (${(a.daten?.aufgaben || []).length})`);
|
||||
}
|
||||
|
||||
console.log("\n=== DogFather teilt Patrick an Mara zu");
|
||||
{
|
||||
const a = await setzen(idPat, idMara, kDogi);
|
||||
ok(a.status === 200, `die Zuteilung geht durch (${a.status})`);
|
||||
|
||||
const l = await holen("/workspace/api/leads", kMara);
|
||||
const namen = (l.daten?.leads || []).map((x) => x.name).sort();
|
||||
ok(namen.length === 1 && namen[0] === "PATRICK-LEAD",
|
||||
`Mara sieht jetzt Patricks Lead (${namen.join(", ")})`);
|
||||
ok(!namen.includes("BEN-LEAD"),
|
||||
" und NICHT den des anderen Scouts");
|
||||
|
||||
/* DIE KETTE -- der eigentliche Punkt der Entscheidung: Sie sieht auch
|
||||
den Creator, den Patrick betreut, obwohl er ihr nie direkt zugeteilt
|
||||
wurde. */
|
||||
const auf = await holen("/workspace/api/aufgaben", kMara);
|
||||
const titel = (auf.daten?.aufgaben || []).map((x) => x.titel).sort();
|
||||
ok(titel.length === 1 && titel[0] === "TILI-AUFGABE",
|
||||
`und die Aufgaben von Patricks Creator (${titel.join(", ")})`);
|
||||
ok(!titel.includes("LUNA-AUFGABE"),
|
||||
" aber nicht die des Creators vom anderen Scout");
|
||||
}
|
||||
|
||||
console.log("\n=== Die zweite Managerin sieht davon nichts");
|
||||
{
|
||||
const l = await holen("/workspace/api/leads", kNina);
|
||||
const a = await holen("/workspace/api/aufgaben", kNina);
|
||||
ok((l.daten?.leads || []).length === 0 && (a.daten?.aufgaben || []).length === 0,
|
||||
`Nina sieht weiterhin nichts (${(l.daten?.leads || []).length} Leads, `
|
||||
+ `${(a.daten?.aufgaben || []).length} Aufgaben) – eine Zuteilung gilt nur fuer den, `
|
||||
+ "dem zugeteilt wurde");
|
||||
}
|
||||
|
||||
console.log("\n=== Auch Suche und Hinweise ziehen mit");
|
||||
{
|
||||
/* Ein Hinweis oder Suchtreffer, der mehr verraet als die Seite
|
||||
dahinter, waere ein Leck -- deshalb dieselbe Regel, aus derselben
|
||||
Quelle. */
|
||||
const s = await holen("/workspace/api/suche?q=LEAD", kMara);
|
||||
const treffer = (s.daten?.gruppen || []).flatMap((g) => g.treffer.map((t) => t.titel || t.name));
|
||||
ok(!treffer.some((t) => String(t).includes("BEN")),
|
||||
`die Suche zeigt Mara keinen fremden Lead (${treffer.join(", ") || "nichts"})`);
|
||||
|
||||
const sN = await holen("/workspace/api/suche?q=LEAD", kNina);
|
||||
const trefferN = (sN.daten?.gruppen || []).flatMap((g) => g.treffer.map((t) => t.titel || t.name));
|
||||
ok(trefferN.length === 0,
|
||||
`und Nina gar keinen (${trefferN.join(", ") || "nichts"})`);
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
3. ZURUECKNEHMEN -- und die Sicht geht wirklich wieder weg
|
||||
================================================================ */
|
||||
console.log("\n=== Zuruecknehmen");
|
||||
{
|
||||
const a = await setzen(idPat, null, kDogi);
|
||||
ok(a.status === 200, `die Zuteilung laesst sich loesen (${a.status})`);
|
||||
const l = await holen("/workspace/api/leads", kMara);
|
||||
const auf = await holen("/workspace/api/aufgaben", kMara);
|
||||
ok((l.daten?.leads || []).length === 0 && (auf.daten?.aufgaben || []).length === 0,
|
||||
`und Mara sieht danach wieder nichts (${(l.daten?.leads || []).length} Leads, `
|
||||
+ `${(auf.daten?.aufgaben || []).length} Aufgaben)`);
|
||||
}
|
||||
|
||||
/* Ein Scout gehoert zu HOECHSTENS einem Manager -- eine zweite Zuteilung
|
||||
ersetzt die erste, sie kommt nicht dazu. Sonst waere "wer ist
|
||||
zustaendig?" eine Frage ohne Antwort. */
|
||||
console.log("\n=== Ein Scout gehoert zu genau einem Manager");
|
||||
{
|
||||
await setzen(idPat, idMara, kDogi);
|
||||
await setzen(idPat, idNina, kDogi);
|
||||
const d2 = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
const z = d2.prepare("SELECT COUNT(*) AS n FROM scout_zuteilung WHERE scout_id = ?").get(idPat);
|
||||
const wer = d2.prepare("SELECT manager_id FROM scout_zuteilung WHERE scout_id = ?").get(idPat);
|
||||
d2.close();
|
||||
ok(z.n === 1, `nach zwei Zuteilungen genau ein Eintrag (${z.n})`);
|
||||
ok(wer.manager_id === idNina, " und zwar der zuletzt gesetzte");
|
||||
|
||||
const lMara = await holen("/workspace/api/leads", kMara);
|
||||
ok((lMara.daten?.leads || []).length === 0,
|
||||
`die erste Managerin sieht Patricks Lead nicht mehr (${(lMara.daten?.leads || []).length})`);
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
4. IM BROWSER: die Auswahl steht da -- und nur bei DogFather
|
||||
================================================================ */
|
||||
console.log("\n=== Die Auswahl in der Personenliste");
|
||||
const { chromium } = await import(
|
||||
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
||||
const browser = await chromium.launch();
|
||||
|
||||
async function seiteAls(rolle, code) {
|
||||
const kontext = await browser.newContext({ viewport: { width: 1280, height: 1100 } });
|
||||
const konsole = [];
|
||||
const seite = await kontext.newPage();
|
||||
seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); });
|
||||
seite.on("pageerror", (e) => konsole.push("pageerror: " + e.message));
|
||||
await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
|
||||
await seite.click(`.rolle[data-rolle="${rolle}"]`);
|
||||
await seite.fill("#code", code);
|
||||
await seite.click("button[type=submit]");
|
||||
await seite.waitForURL("**/start.html", { timeout: 15000 });
|
||||
await seite.goto(BASIS + "/workspace/personen.html", { waitUntil: "networkidle" });
|
||||
await seite.waitForTimeout(1200);
|
||||
/* DIE LISTE IST NACH ROLLEN ZUGEKLAPPT -- offen ist nur die eigene
|
||||
Gruppe. Der erste Anlauf dieser Pruefung mass deshalb genau eine
|
||||
Zeile ("Filipe (du)") und meldete, die Auswahl fehle. Sie fehlte
|
||||
nicht, sie war nur zugeklappt.
|
||||
|
||||
Ein lehrreicher Fehlschlag: Er sah aus wie ein Produktfehler und war
|
||||
einer der Pruefung. Wer ihn ungeprueft geglaubt haette, haette am
|
||||
falschen Ende repariert. */
|
||||
await seite.click("#rest-schalter").catch(() => {});
|
||||
await seite.waitForTimeout(600);
|
||||
return { kontext, seite, konsole };
|
||||
}
|
||||
|
||||
const bDogi = await seiteAls("admin", "CODE-DOGI-0001");
|
||||
const r = await bDogi.seite.evaluate(() => ({
|
||||
personen: document.querySelectorAll(".person").length,
|
||||
/* Die Auswahl "Gehört zu" -- eine je Scout. */
|
||||
wahlen: [...document.querySelectorAll('select[id^="manager-"]')].map((s) => ({
|
||||
id: s.id,
|
||||
wert: s.value,
|
||||
erste: s.querySelector("option")?.textContent,
|
||||
anzahl: s.querySelectorAll("option").length,
|
||||
})),
|
||||
/* Die Zeilen sollen es auch in Worten sagen. */
|
||||
texte: [...document.querySelectorAll(".person__zeile")].map((z) => z.textContent),
|
||||
ueber: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
||||
}));
|
||||
ok(r.personen === 7,
|
||||
`aufgeklappt stehen alle sieben Personen da (${r.personen}) – `
|
||||
+ "sonst misst die Pruefung eine zugeklappte Liste");
|
||||
ok(r.wahlen.length === 2, `zwei Scouts, zwei Auswahlfelder (${r.wahlen.length})`);
|
||||
ok(/direkt bei DogFather/.test(r.wahlen[0]?.erste || ""),
|
||||
`der Leer-Wert heisst, was er ist ("${r.wahlen[0]?.erste}") – `
|
||||
+ '"niemand" waere falsch, ein Scout ohne Manager haengt an dir');
|
||||
ok(r.wahlen.some((w) => w.wert === String(idNina)),
|
||||
"die bestehende Zuteilung ist vorausgewaehlt");
|
||||
ok(r.texte.some((t) => /gehört zu Nina/.test(t)),
|
||||
"und steht auch in Worten in der Zeile");
|
||||
ok(r.texte.some((t) => /1 Scout zugeteilt/.test(t)),
|
||||
"beim Manager steht die Zahl seiner Scouts");
|
||||
ok(r.ueber <= 0, `nichts steht ueber (${r.ueber}px)`);
|
||||
ok(bDogi.konsole.length === 0,
|
||||
`keine Fehler in der Konsole${bDogi.konsole.length ? " -> " + bDogi.konsole.join(" | ") : ""}`);
|
||||
await bDogi.seite.screenshot({ path: "pruef-scout-zuteilung.png", fullPage: false });
|
||||
await bDogi.kontext.close();
|
||||
|
||||
/* GEGENPROBE: Ein Manager darf die Personenliste sehen -- aber diese
|
||||
Auswahl nicht. */
|
||||
const bMara = await seiteAls("manager", "CODE-MANA-0001");
|
||||
const rM = await bMara.seite.evaluate(() => ({
|
||||
wahlen: document.querySelectorAll('select[id^="manager-"]').length,
|
||||
personen: document.querySelectorAll(".person").length,
|
||||
}));
|
||||
ok(rM.personen >= 7,
|
||||
`ein Manager sieht die ganze Personenliste (${rM.personen} Zeilen) – `
|
||||
+ "die Personenverwaltung ist ihm nicht genommen worden");
|
||||
ok(rM.wahlen === 0,
|
||||
`Gegenprobe: aber KEINE Zuteilungs-Auswahl (${rM.wahlen}) – er koennte sich sonst `
|
||||
+ "seine eigene Sichtbarkeit vergeben");
|
||||
await bMara.kontext.close();
|
||||
|
||||
await browser.close();
|
||||
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
||||
console.log(fehler ? `\n${fehler} Prüfung(en) fehlgeschlagen.` : "\nAlles in Ordnung.");
|
||||
process.exit(fehler ? 1 : 0);
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
import express from "express";
|
||||
import {
|
||||
db, sitzungLesen, betreuteIds, istLeitung,
|
||||
db, sitzungLesen, betreuteIds, istLeitung, istDogFather, pipelineIds,
|
||||
} from "./workspace.js";
|
||||
import { sichtbar as sichtbarAufgaben } from "./workspace-aufgaben.js";
|
||||
import { sichtbar as sichtbarTermine } from "./workspace-kalender.js";
|
||||
@@ -204,9 +204,16 @@ hinweisRouter.get("/workspace/api/hinweise", (req, res) => {
|
||||
}
|
||||
|
||||
/* ---------- Scout-Pipeline ---------- */
|
||||
if (istLeitung(person) || person.rolle === "scout") {
|
||||
const nur = istLeitung(person) ? "1=1" : "l.scout_id = ?";
|
||||
const werte = istLeitung(person) ? [] : [person.id];
|
||||
/* NUR DogFather sieht alle Leads (01.09.2026). Ein Scout seine
|
||||
eigenen, ein Manager seine eigenen und die seiner zugeteilten
|
||||
Scouts -- dieselbe Regel wie in der Pipeline selbst, aus
|
||||
derselben Quelle (pipelineIds in workspace.js). Ein Hinweis, der
|
||||
mehr verraet als die Seite dahinter, waere ein Leck. */
|
||||
if (istDogFather(person) || person.rolle === "scout" || person.rolle === "manager") {
|
||||
const ids = istDogFather(person) ? null : pipelineIds(person);
|
||||
const nur = ids === null ? "1=1"
|
||||
: ids.length ? `l.scout_id IN (${ids.map(() => "?").join(",")})` : "0=1";
|
||||
const werte = ids === null ? [] : ids;
|
||||
dazu("followup_faellig", "warnung",
|
||||
"Follow-up fällig", "scouting.html",
|
||||
zaehle(`SELECT COUNT(*) n FROM leads l
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
import express from "express";
|
||||
import {
|
||||
db, protokolliere, echteIp, sitzungLesen, personAnlegen, codeNeu, personSperren, betreuungSetzen, istLeitung, istDogFather, ROLLEN_SORTIERUNG, ROLLEN_REIHE,
|
||||
db, protokolliere, echteIp, sitzungLesen, personAnlegen, codeNeu, personSperren, betreuungSetzen, scoutZuteilungSetzen, istLeitung, istDogFather, ROLLEN_SORTIERUNG, ROLLEN_REIHE,
|
||||
} from "./workspace.js";
|
||||
import { sicherungJetzt } from "./workspace-sicherung.js";
|
||||
|
||||
@@ -98,9 +98,18 @@ personenRouter.get("/workspace/api/verwaltung/personen", (req, res) => {
|
||||
AND a.status <> 'erledigt') AS offene_aufgaben,
|
||||
b.betreuer_id,
|
||||
(SELECT name FROM personen x WHERE x.id = b.betreuer_id) AS betreuer_name,
|
||||
(SELECT COUNT(*) FROM betreuung y WHERE y.betreuer_id = p.id) AS betreut_anzahl
|
||||
(SELECT COUNT(*) FROM betreuung y WHERE y.betreuer_id = p.id) AS betreut_anzahl,
|
||||
/* Manager -> Scouts (01.09.2026). Bei einem Scout steht
|
||||
hier, wem er zugeteilt ist; bei einem Manager, wie viele
|
||||
Scouts an ihm haengen. Beides aus derselben Abfrage --
|
||||
eine zweite waere eine zweite Gelegenheit, dass die
|
||||
Liste etwas anderes sagt als die Rechte. */
|
||||
sz.manager_id,
|
||||
(SELECT name FROM personen m WHERE m.id = sz.manager_id) AS manager_name,
|
||||
(SELECT COUNT(*) FROM scout_zuteilung z WHERE z.manager_id = p.id) AS scouts_anzahl
|
||||
FROM personen p
|
||||
LEFT JOIN betreuung b ON b.creator_id = p.id
|
||||
LEFT JOIN scout_zuteilung sz ON sz.scout_id = p.id
|
||||
ORDER BY ${ROLLEN_SORTIERUNG.replace("rolle", "p.rolle")}, p.aktiv DESC, p.name`).all(),
|
||||
/* Wer ueberhaupt als zustaendig eingetragen werden kann.
|
||||
|
||||
@@ -151,11 +160,17 @@ personenRouter.put("/workspace/api/verwaltung/betreuung/:id", gleicheHerkunft, (
|
||||
/* DogFather, Manager und Scouts. Ein Creator kann nicht fuer einen
|
||||
anderen zustaendig sein -- das waere eine Rolle, die es nicht gibt.
|
||||
|
||||
WICHTIG: Ein Eintrag auf DogFather oder Manager aendert an den
|
||||
RECHTEN nichts. Die Leitung sieht ohnehin jeden Creator; der
|
||||
Eintrag sagt nur, WER sich kuemmert. Nur beim Scout entscheidet er
|
||||
zusaetzlich ueber die Sichtbarkeit -- deshalb steht die Regel
|
||||
dafuer unveraendert in betreuteIds(). */
|
||||
ACHTUNG, SEIT DEM 01.09.2026 ANDERS: Hier stand frueher "ein
|
||||
Eintrag auf DogFather oder Manager aendert an den Rechten nichts,
|
||||
die Leitung sieht ohnehin jeden Creator". Das gilt nicht mehr --
|
||||
nur noch DogFather sieht alles. Fuer einen MANAGER entscheidet
|
||||
dieser Eintrag jetzt genauso ueber die Sichtbarkeit wie fuer einen
|
||||
Scout: Wer ihm nicht zugeteilt ist (weder direkt noch ueber einen
|
||||
seiner Scouts), taucht bei ihm nicht auf.
|
||||
|
||||
Nur bei DogFather sagt der Eintrag weiterhin bloss, WER sich
|
||||
kuemmert. Die Regel dazu steht an genau einer Stelle:
|
||||
betreuteIds() in workspace.js. */
|
||||
if (!betreuer || !["admin", "manager", "scout"].includes(betreuer.rolle)) {
|
||||
return res.status(400).json({
|
||||
fehler: "Zuständig können nur aktive DogFather, Manager oder Scouts sein.",
|
||||
@@ -170,6 +185,63 @@ personenRouter.put("/workspace/api/verwaltung/betreuung/:id", gleicheHerkunft, (
|
||||
}
|
||||
});
|
||||
|
||||
/* ---------- Welcher Scout gehoert zu welchem Manager --------------------
|
||||
|
||||
"er soll nur die Scouts sehen, die ihm zugeteilt sind." (01.09.2026)
|
||||
|
||||
NUR DOGFATHER darf das setzen -- ausdruecklich so entschieden, und
|
||||
der Grund ist nicht Misstrauen, sondern Bauart: Diese Zuteilung
|
||||
ERWEITERT die Sicht eines Managers (auf die Leads seiner Scouts und
|
||||
auf deren Creator). Duerfte er sie selbst setzen, koennte er sich
|
||||
seine eigene Sichtbarkeit vergeben -- und eine Grenze, die der
|
||||
Begrenzte selbst verschieben kann, ist keine.
|
||||
|
||||
Deshalb istDogFather und NICHT istLeitung. Der Unterschied ist hier
|
||||
der ganze Punkt. */
|
||||
|
||||
personenRouter.put("/workspace/api/verwaltung/scout-zuteilung/:id", gleicheHerkunft, (req, res) => {
|
||||
try {
|
||||
if (!istDogFather(req.person)) {
|
||||
/* 404 und nicht 403: Wer es nicht darf, soll nicht einmal
|
||||
erfahren, dass es diesen Weg gibt. */
|
||||
return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
}
|
||||
const id = Number(req.params.id);
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ fehler: "ungueltig" });
|
||||
|
||||
const scout = db().prepare("SELECT id, rolle FROM personen WHERE id = ?").get(id);
|
||||
if (!scout || scout.rolle !== "scout") {
|
||||
return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
}
|
||||
|
||||
const w = req.body?.manager_id;
|
||||
if (w === null || w === "" || w === undefined) {
|
||||
scoutZuteilungSetzen(id, null, req.person.id);
|
||||
protokolliere("scout_zuteilung", req.person, echteIp(req), `${id}: geloest`);
|
||||
return res.json({ ok: true, manager_id: null });
|
||||
}
|
||||
|
||||
const z = Number(w);
|
||||
if (!Number.isInteger(z) || z < 1) return res.status(400).json({ fehler: "Ungültige Auswahl." });
|
||||
const manager = db().prepare(
|
||||
"SELECT id, name, rolle FROM personen WHERE id = ? AND aktiv = 1").get(z);
|
||||
/* NUR ein Manager. Ein Scout unter einem Scout waere eine Ordnung,
|
||||
die es nicht gibt; ein Scout unter DogFather ist ueberfluessig, da
|
||||
er ohnehin alles sieht -- und ein Eintrag, der nichts bewirkt,
|
||||
wird beim naechsten Mal fuer eine Wirkung gehalten. */
|
||||
if (!manager || manager.rolle !== "manager") {
|
||||
return res.status(400).json({ fehler: "Zuteilen kann man nur an einen aktiven Manager." });
|
||||
}
|
||||
|
||||
scoutZuteilungSetzen(id, z, req.person.id);
|
||||
protokolliere("scout_zuteilung", req.person, echteIp(req), `${id} -> ${z}`);
|
||||
res.json({ ok: true, manager_id: z, manager_name: manager.name });
|
||||
} catch (fehler) {
|
||||
console.error("[workspace] Scout-Zuteilung:", fehler?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
/* ---------- Anlegen ----------------------------------------------------- */
|
||||
|
||||
personenRouter.post("/workspace/api/verwaltung/personen", gleicheHerkunft, (req, res) => {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
import express from "express";
|
||||
import {
|
||||
db, protokolliere, echteIp, sitzungLesen, personAnlegen, betreuungSetzen, istLeitung,
|
||||
db, protokolliere, echteIp, sitzungLesen, personAnlegen, betreuungSetzen, istLeitung, istDogFather, pipelineIds,
|
||||
} from "./workspace.js";
|
||||
|
||||
export const scoutRouter = express.Router();
|
||||
@@ -57,9 +57,23 @@ function gleicheHerkunft(req, res, next) {
|
||||
|
||||
scoutRouter.use("/workspace/api/leads", angemeldet);
|
||||
|
||||
/* Wessen Pipeline sieht diese Person?
|
||||
|
||||
NUR DogFather sieht alle (01.09.2026). Ein Scout sieht seine
|
||||
eigenen Leads, ein Manager seine eigenen UND die der Scouts, die
|
||||
ihm zugeteilt sind: "er soll nur die Scouts sehen, die ihm
|
||||
zugeteilt sind."
|
||||
|
||||
Vorher stand hier istLeitung() -- damit sah jeder Manager jeden
|
||||
Lead jedes Scouts. pipelineIds() steht in workspace.js und wird von
|
||||
drei Stellen benutzt (Liste, Suche, Hinweise); dieselbe Regel
|
||||
dreimal abgeschrieben waere dreimal Gelegenheit, sie einmal falsch
|
||||
zu aendern. */
|
||||
function sichtbar(person) {
|
||||
if (istLeitung(person)) return { wo: "1=1", werte: [] };
|
||||
return { wo: "l.scout_id = ?", werte: [person.id] };
|
||||
if (istDogFather(person)) return { wo: "1=1", werte: [] };
|
||||
const ids = pipelineIds(person);
|
||||
if (!ids.length) return { wo: "0=1", werte: [] };
|
||||
return { wo: `l.scout_id IN (${ids.map(() => "?").join(",")})`, werte: ids };
|
||||
}
|
||||
|
||||
const SPALTEN = `
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
import express from "express";
|
||||
import {
|
||||
db, sitzungLesen, betreuteIds, istLeitung,
|
||||
db, sitzungLesen, betreuteIds, istLeitung, istDogFather, pipelineIds,
|
||||
} from "./workspace.js";
|
||||
import { sichtbar as sichtbarAufgaben } from "./workspace-aufgaben.js";
|
||||
import { sichtbar as sichtbarTermine } from "./workspace-kalender.js";
|
||||
@@ -186,9 +186,16 @@ sucheRouter.get("/workspace/api/suche", (req, res) => {
|
||||
}
|
||||
|
||||
/* ---------- Scout-Pipeline ---------- */
|
||||
if (istLeitung(person) || person.rolle === "scout") {
|
||||
const nur = istLeitung(person) ? "1=1" : "l.scout_id = ?";
|
||||
const werte = istLeitung(person) ? [] : [person.id];
|
||||
/* NUR DogFather sieht alle Leads (01.09.2026). Ein Scout seine
|
||||
eigenen, ein Manager seine eigenen und die seiner zugeteilten
|
||||
Scouts -- dieselbe Regel wie in der Pipeline selbst, aus
|
||||
derselben Quelle (pipelineIds in workspace.js). Ein Hinweis, der
|
||||
mehr verraet als die Seite dahinter, waere ein Leck. */
|
||||
if (istDogFather(person) || person.rolle === "scout" || person.rolle === "manager") {
|
||||
const ids = istDogFather(person) ? null : pipelineIds(person);
|
||||
const nur = ids === null ? "1=1"
|
||||
: ids.length ? `l.scout_id IN (${ids.map(() => "?").join(",")})` : "0=1";
|
||||
const werte = ids === null ? [] : ids;
|
||||
quelle("Scout-Pipeline", `
|
||||
SELECT l.id, l.name, l.plattform, l.handle, l.status, l.notizen, l.potenzial, l.aktivitaet
|
||||
FROM leads l
|
||||
|
||||
@@ -553,6 +553,34 @@ export function db() {
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_betreuung_betreuer ON betreuung (betreuer_id);
|
||||
|
||||
/* Welcher Scout gehoert zu welchem Manager (01.09.2026).
|
||||
|
||||
"er soll nur die Scouts sehen, die ihm zugeteilt sind."
|
||||
|
||||
WARUM EINE EIGENE TABELLE und nicht ein zweiter Eintrag in
|
||||
betreuung: Dort heisst die Spalte creator_id, und der ganze
|
||||
uebrige Code liest sie als "das ist ein Creator". Ein Scout in
|
||||
dieser Spalte waere technisch moeglich und fachlich eine Luege --
|
||||
betreuteIds() gaebe dann Scout-Nummern zurueck, die anderswo als
|
||||
Creator behandelt wuerden. Solche Abkuerzungen raechen sich
|
||||
genau dann, wenn niemand mehr weiss, dass sie getroffen wurden.
|
||||
|
||||
Ein Scout gehoert zu hoechstens EINEM Manager -- deshalb ist
|
||||
scout_id der Schluessel. Eine Zuteilung an mehrere waere die
|
||||
Frage "wer ist zustaendig?" ohne Antwort.
|
||||
|
||||
ON DELETE CASCADE auf beiden Seiten: Verschwindet der Scout oder
|
||||
der Manager, ist die Zuteilung gegenstandslos. Sie stehenzulassen
|
||||
hiesse, dass eine geloeschte Person weiter Sichtbarkeit steuert. */
|
||||
CREATE TABLE IF NOT EXISTS scout_zuteilung (
|
||||
scout_id INTEGER PRIMARY KEY REFERENCES personen(id) ON DELETE CASCADE,
|
||||
manager_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||||
seit TEXT NOT NULL,
|
||||
gesetzt_von INTEGER REFERENCES personen(id) ON DELETE SET NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_scout_zuteilung_manager
|
||||
ON scout_zuteilung (manager_id);
|
||||
|
||||
/* Meeting-Protokoll zu einem Termin (Konzept, Seite 13).
|
||||
Bewusst eine eigene Tabelle statt neuer Spalten in termine: Es
|
||||
gibt hier keine Migrationen, und ein Protokoll gehoert ohnehin
|
||||
@@ -909,13 +937,75 @@ export function codeErzeugen(gruppen = 4, laenge = 4) {
|
||||
export function betreuteIds(person) {
|
||||
if (!person || (person.rolle !== "scout" && person.rolle !== "manager")) return [];
|
||||
try {
|
||||
return db().prepare("SELECT creator_id FROM betreuung WHERE betreuer_id = ?")
|
||||
const eigene = db().prepare("SELECT creator_id FROM betreuung WHERE betreuer_id = ?")
|
||||
.all(person.id).map((z) => z.creator_id);
|
||||
if (person.rolle !== "manager") return eigene;
|
||||
|
||||
/* DIE KETTE: Manager -> seine Scouts -> deren Creator (01.09.2026).
|
||||
|
||||
Entschieden auf die Frage "sieht er dann auch die Creator dieser
|
||||
Scouts?" -- "ja, alles seiner Scouts". Das ist die uebliche
|
||||
Ordnung: Wer einen Scout fuehrt, muss sehen, woran der arbeitet.
|
||||
Ohne das muesste jeder Creator einem Manager EINZELN zugewiesen
|
||||
werden, und beim ersten vergessenen faende er ein Loch in seiner
|
||||
Uebersicht, ohne zu merken, dass es eines ist.
|
||||
|
||||
Ein Set, weil ein Creator auf beiden Wegen kommen kann: direkt
|
||||
zugeteilt UND ueber seinen Scout. Doppelte Nummern wuerden in den
|
||||
IN-Listen zu doppelten Fragezeichen -- fachlich harmlos, aber
|
||||
jede Abfrage unnoetig laenger. */
|
||||
const scouts = scoutsVon(person.id);
|
||||
if (!scouts.length) return eigene;
|
||||
const ueberScouts = db().prepare(
|
||||
`SELECT creator_id FROM betreuung
|
||||
WHERE betreuer_id IN (${scouts.map(() => "?").join(",")})`)
|
||||
.all(...scouts).map((z) => z.creator_id);
|
||||
return [...new Set([...eigene, ...ueberScouts])];
|
||||
} catch {
|
||||
return []; // im Zweifel nichts sehen, nie mehr
|
||||
}
|
||||
}
|
||||
|
||||
/** Die Scouts, die diesem Manager zugeteilt sind. Fuer alle anderen
|
||||
* leer -- ein Scout fuehrt keine Scouts, ein Creator schon gar nicht.
|
||||
* DogFather braucht sie nicht: Er sieht ohnehin alles. */
|
||||
export function scoutsVon(managerId) {
|
||||
if (!managerId) return [];
|
||||
try {
|
||||
return db().prepare("SELECT scout_id FROM scout_zuteilung WHERE manager_id = ?")
|
||||
.all(managerId).map((z) => z.scout_id);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/** Wessen Leads darf diese Person sehen? Gibt die Personennummern
|
||||
* zurueck, deren Pipeline sichtbar ist -- der eigene immer dabei.
|
||||
* Ein Scout sieht nur sich, ein Manager sich und seine Scouts. */
|
||||
export function pipelineIds(person) {
|
||||
if (!person) return [];
|
||||
if (person.rolle !== "manager") return [person.id];
|
||||
return [...new Set([person.id, ...scoutsVon(person.id)])];
|
||||
}
|
||||
|
||||
/** Zuteilung setzen oder loesen (null loest sie).
|
||||
* Wer das DARF, entscheidet die Route -- hier steht nur, WIE. */
|
||||
export function scoutZuteilungSetzen(scoutId, managerId, akteur = null) {
|
||||
const d = db();
|
||||
if (managerId === null) {
|
||||
d.prepare("DELETE FROM scout_zuteilung WHERE scout_id = ?").run(scoutId);
|
||||
return;
|
||||
}
|
||||
d.prepare(`
|
||||
INSERT INTO scout_zuteilung (scout_id, manager_id, seit, gesetzt_von)
|
||||
VALUES (?,?,?,?)
|
||||
ON CONFLICT(scout_id) DO UPDATE SET
|
||||
manager_id = excluded.manager_id,
|
||||
seit = excluded.seit,
|
||||
gesetzt_von = excluded.gesetzt_von`)
|
||||
.run(scoutId, managerId, new Date().toISOString(), akteur);
|
||||
}
|
||||
|
||||
/* SQL-Baustein "diese Spalte gehoert zu einem meiner Creator".
|
||||
Gibt null zurueck, wenn es nichts zu ergaenzen gibt -- eine leere
|
||||
IN-Liste waere ungueltiges SQL. */
|
||||
|
||||
@@ -137,6 +137,21 @@
|
||||
if (p.betreut_anzahl) {
|
||||
z.append(el('span', null, `betreut ${p.betreut_anzahl} Creator`));
|
||||
}
|
||||
/* Bei einem Manager: wie viele Scouts an ihm haengen. Ohne diese Zahl
|
||||
muesste man die ganze Liste durchgehen, um zu sehen, ob eine
|
||||
Zuteilung ueberhaupt angekommen ist.
|
||||
|
||||
Steht in DERSELBEN Zeile wie "betreut N Creator" -- ein eigenes
|
||||
Element haette eine eigene Klasse gebraucht, die es im Stil nicht
|
||||
gibt, und waere als nackter Text erschienen. */
|
||||
if (p.rolle === 'manager' && p.scouts_anzahl) {
|
||||
z.append(el('span', null,
|
||||
`${p.scouts_anzahl} ${p.scouts_anzahl === 1 ? 'Scout' : 'Scouts'} zugeteilt`));
|
||||
}
|
||||
/* Beim Scout umgekehrt: zu wem er gehoert. */
|
||||
if (p.rolle === 'scout' && p.manager_name) {
|
||||
z.append(el('span', null, `gehört zu ${p.manager_name}`));
|
||||
}
|
||||
links.append(z);
|
||||
|
||||
/* Zuständigkeit. Steht nur bei Creator und nur dann, wenn es
|
||||
@@ -194,6 +209,51 @@
|
||||
links.append(b);
|
||||
}
|
||||
|
||||
/* ---- Welchem Manager gehoert dieser Scout? (01.09.2026) ----
|
||||
|
||||
"er soll nur die Scouts sehen, die ihm zugeteilt sind."
|
||||
|
||||
NUR fuer DogFather sichtbar -- und das ist kein Schoenheitsgrund:
|
||||
Diese Zuteilung ERWEITERT die Sicht eines Managers (auf die Leads
|
||||
seiner Scouts und auf deren Creator). Duerfte er sie selbst
|
||||
setzen, koennte er sich seine eigene Sichtbarkeit vergeben. Der
|
||||
Server weist es ohnehin ab; hier geht es nur darum, niemandem
|
||||
einen Knopf hinzustellen, der nur eine Absage bringt.
|
||||
|
||||
Und nur, wenn es ueberhaupt einen Manager gibt -- eine leere
|
||||
Auswahl sieht aus wie ein Fehler. */
|
||||
const managerListe = betreuerListe.filter((x) => x.rolle === 'manager');
|
||||
if (p.rolle === 'scout' && ich.rolle === 'admin' && managerListe.length) {
|
||||
const b = el('div', 'betreuung');
|
||||
const s2 = el('label', 'betreuung__schild', 'Gehört zu');
|
||||
s2.htmlFor = 'manager-' + p.id;
|
||||
const w2 = el('select', 'betreuung__wahl');
|
||||
w2.id = 'manager-' + p.id;
|
||||
const leer2 = document.createElement('option');
|
||||
leer2.value = '';
|
||||
/* Nicht "— niemand —": Ein Scout ohne Manager ist nicht
|
||||
unbetreut, er haengt direkt an dir. Das ist ein Zustand, kein
|
||||
Mangel. */
|
||||
leer2.textContent = '— direkt bei DogFather —';
|
||||
w2.append(leer2);
|
||||
for (const m of managerListe) w2.append(new Option(m.name, String(m.id)));
|
||||
w2.value = p.manager_id ? String(p.manager_id) : '';
|
||||
w2.addEventListener('change', async () => {
|
||||
w2.disabled = true; melde('');
|
||||
try {
|
||||
const a = await hole(`/workspace/api/verwaltung/scout-zuteilung/${p.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ manager_id: w2.value || null }),
|
||||
});
|
||||
if (!a.ok) { melde((await a.json().catch(() => ({}))).fehler || 'Ging nicht.'); return; }
|
||||
await laden();
|
||||
} catch { /* umgeleitet */ } finally { w2.disabled = false; }
|
||||
});
|
||||
b.append(s2, w2);
|
||||
links.append(b);
|
||||
}
|
||||
|
||||
const knoepfe = el('div', 'person__knoepfe');
|
||||
knoepfe.append(tuKnopf('Neuer Code', async () => {
|
||||
const eigen = p.id === ich.id;
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
<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?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012233" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -152,9 +152,9 @@
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/aufgaben.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/aufgaben.js?v=202609012233" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
<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?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/automation.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/automation.css?v=202609012233" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -132,9 +132,9 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/automation.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/automation.js?v=202609012233" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
<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?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/bereich.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/bereich.css?v=202609012233" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -115,13 +115,13 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/checkliste.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/ampel.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/schulung.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/vorlagen.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/bereich.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/checkliste.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/ampel.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/schulung.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/vorlagen.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/bereich.js?v=202609012233" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
<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?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/calls.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/calls.css?v=202609012233" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -54,10 +54,10 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/ki.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/calls.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/ki.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/calls.js?v=202609012233" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
<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?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/content.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/content.css?v=202609012233" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -164,11 +164,11 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/checkliste.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/vorlagen.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/content.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/checkliste.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/vorlagen.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/content.js?v=202609012233" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
<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?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/dateien.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/dateien.css?v=202609012233" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -93,9 +93,9 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/dateien.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/dateien.js?v=202609012233" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
type="image/webp" media="(max-width: 899px)" />
|
||||
|
||||
<link rel="icon" href="assets/img/favicon.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609012233" />
|
||||
</head>
|
||||
|
||||
<body class="gate">
|
||||
@@ -143,6 +143,6 @@
|
||||
</p>
|
||||
</main>
|
||||
|
||||
<script src="assets/js/gate.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/gate.js?v=202609012233" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
<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?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/kalender.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/kalender.css?v=202609012233" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -147,9 +147,9 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/kalender.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/kalender.js?v=202609012233" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
<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?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/personen.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/personen.css?v=202609012233" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -121,9 +121,9 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/personen.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/personen.js?v=202609012233" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
<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?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/profil.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/profil.css?v=202609012233" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -193,10 +193,10 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/steckbrief.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/profil.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/steckbrief.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/profil.js?v=202609012233" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
<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?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/report.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/report.css?v=202609012233" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -91,10 +91,10 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/ki.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/report.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/ki.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/report.js?v=202609012233" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
<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?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/scouting.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/scouting.css?v=202609012233" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -124,9 +124,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/scouting.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/scouting.js?v=202609012233" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
<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?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609012233" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -99,9 +99,9 @@
|
||||
</p>
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/start.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/start.js?v=202609012233" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
<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?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609012233" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -70,10 +70,10 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/ki.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/startcheck.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/ki.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/startcheck.js?v=202609012233" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
<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?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/profil.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/profil.css?v=202609012233" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -111,9 +111,9 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/steckbrief.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/steckbrief.js?v=202609012233" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
<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?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609012233" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -57,9 +57,9 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/bereiche.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609012233" defer></script>
|
||||
|
||||
<script src="assets/js/kopf.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/uebersicht.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/uebersicht.js?v=202609012233" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
<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?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/wissen.css?v=202609012128" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012233" />
|
||||
<link rel="stylesheet" href="assets/css/wissen.css?v=202609012233" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -175,9 +175,9 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/wissen.js?v=202609012128" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609012233" defer></script>
|
||||
<script src="assets/js/wissen.js?v=202609012233" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||