Workspace: Creator-Profile (Onboarding aus dem Konzept)
/workspace/profil.html -- Stammdaten, Ziele und 90-Tage-Plan, genau nach Seite 5 des Konzepts. Management waehlt oben den Creator aus, ein Creator sieht nur sein eigenes Profil. Sicherheitskern ist das Feld admin_notiz. Das Konzept fordert "private Admin-Notizen separat". Die Notiz wird deshalb nicht im Browser ausgeblendet, sondern gar nicht erst gesendet: Die Spaltenliste der Abfrage haengt an der Rolle (FELDER_OFFEN / FELDER_ADMIN). Dasselbe gilt fuer Plan-Start und Review-Termin. Geprueft: - In der kompletten Rohantwort an den Creator kommt der Inhalt der internen Notiz 0-mal vor - Creator auf fremdes Profil: 404 (lesend wie schreibend) - Scout auf ein Profil: 404, profil.html leitet ihn weg - Creator setzt admin_notiz selbst: wird stillschweigend ignoriert, der Inhalt bleibt unveraendert - Profil einer Nicht-Creator-Person: 404 Dabei ist ein aelterer Fehler aufgefallen: /api/ich lieferte nur Name und Rolle, nicht die eigene Nummer. Dadurch rief die Profilseite eines Creators /api/profil/undefined auf und blieb leer. Derselbe Fehler machte in der Personenverwaltung den Selbstvergleich unwirksam -- beim eigenen Eintrag erschien ein "Sperren"-Knopf, den der Server dann ablehnte. /api/ich liefert jetzt zusaetzlich die id. Im Protokoll landen nur die Feldnamen, nie die Inhalte: Im Profil stehen persoenliche Angaben, die nicht zusaetzlich im Audit-Log auftauchen sollen.
This commit is contained in:
@@ -16,6 +16,7 @@ import { inhaltsrichtlinie } from "./inhaltsrichtlinie.js";
|
||||
import { workspaceRouter } from "./workspace.js";
|
||||
import { aufgabenRouter } from "./workspace-aufgaben.js";
|
||||
import { personenRouter } from "./workspace-personen.js";
|
||||
import { profilRouter } from "./workspace-profil.js";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const SITE_DIR = join(__dirname, "..");
|
||||
@@ -72,6 +73,7 @@ app.use(webdesignGate);
|
||||
app.use(workspaceRouter);
|
||||
app.use(aufgabenRouter);
|
||||
app.use(personenRouter);
|
||||
app.use(profilRouter);
|
||||
|
||||
/* Sicherheits-Fix 20.08.2026 (Audit vor dem geplanten öffentlichen Start morgen).
|
||||
SITE_DIR ist der GESAMTE Repo-Ordner (join(__dirname, "..")) — express.static liefert
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
/* =====================================================================
|
||||
workspace-profil.js — Creator-Profile (Onboarding-Teil des Konzepts):
|
||||
Stammdaten, Ziele, 90-Tage-Plan.
|
||||
|
||||
Sicherheitskern dieses Moduls ist das Feld `admin_notiz`. Das Konzept
|
||||
fordert "private Admin-Notizen separat" -- diese Notiz darf ein Creator
|
||||
unter keinen Umständen sehen, auch nicht als Nebeneffekt einer
|
||||
Sammelabfrage. Deshalb wird sie nicht nur im Browser ausgeblendet,
|
||||
sondern gar nicht erst mitgeschickt: Die Spaltenliste der Abfrage
|
||||
haengt an der Rolle (siehe FELDER_OFFEN / FELDER_ADMIN).
|
||||
===================================================================== */
|
||||
|
||||
import express from "express";
|
||||
import { db, protokolliere, echteIp, sitzungLesen } from "./workspace.js";
|
||||
|
||||
export const profilRouter = express.Router();
|
||||
|
||||
/* Felder, die auch der Creator selbst sehen und ändern darf. */
|
||||
const FELDER_OFFEN = [
|
||||
"handles", "nische", "live_zeiten", "technik",
|
||||
"ziel_live", "ziel_content", "ziel_community", "ziel_technik",
|
||||
"plan_prio1", "plan_prio2", "plan_prio3",
|
||||
];
|
||||
/* Nur fürs Management. `admin_notiz` ist der eigentliche Grund für die
|
||||
Trennung, die Termine stehen hier, weil sie zur Steuerung gehören. */
|
||||
const FELDER_ADMIN = ["plan_start", "naechster_review", "admin_notiz"];
|
||||
|
||||
const DATUMSFELDER = new Set(["plan_start", "naechster_review"]);
|
||||
const TEXT_MAX = 2000;
|
||||
|
||||
const jetzt = () => new Date().toISOString();
|
||||
|
||||
function angemeldet(req, res, next) {
|
||||
const person = sitzungLesen(req);
|
||||
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
|
||||
req.person = person;
|
||||
next();
|
||||
}
|
||||
|
||||
function gleicheHerkunft(req, res, next) {
|
||||
const herkunft = req.get("origin");
|
||||
if (!herkunft) return next();
|
||||
let erlaubt;
|
||||
try { erlaubt = new URL(herkunft).host === req.get("host"); } catch { erlaubt = false; }
|
||||
if (!erlaubt) return res.status(403).json({ fehler: "fremde_herkunft" });
|
||||
next();
|
||||
}
|
||||
|
||||
profilRouter.use("/workspace/api/profil", angemeldet);
|
||||
|
||||
/* Wer darf welches Profil? Scouts gar keins -- im Konzept haben sie mit
|
||||
der Creator-Betreuung nichts zu tun. */
|
||||
function darf(person, zielId) {
|
||||
if (person.rolle === "admin") return "voll";
|
||||
if (person.rolle === "creator" && person.id === zielId) return "eigen";
|
||||
return null;
|
||||
}
|
||||
|
||||
/* ---------- Lesen ------------------------------------------------------- */
|
||||
|
||||
profilRouter.get("/workspace/api/profil/:id", (req, res) => {
|
||||
try {
|
||||
const id = Number(req.params.id);
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ fehler: "ungueltig" });
|
||||
|
||||
const recht = darf(req.person, id);
|
||||
/* 404 statt 403: Wer kein Recht hat, soll nicht erfahren, ob es die
|
||||
Person überhaupt gibt. */
|
||||
if (!recht) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
|
||||
const person = db().prepare(
|
||||
"SELECT id, name, rolle, aktiv, erstellt, letzter_login FROM personen WHERE id = ?").get(id);
|
||||
if (!person || person.rolle !== "creator") {
|
||||
return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
}
|
||||
|
||||
const felder = recht === "voll" ? [...FELDER_OFFEN, ...FELDER_ADMIN] : FELDER_OFFEN;
|
||||
const reihe = db().prepare(
|
||||
`SELECT ${felder.join(", ")}, geaendert FROM profile WHERE person_id = ?`).get(id);
|
||||
|
||||
res.json({
|
||||
person,
|
||||
/* Kein Profil angelegt = leeres Objekt, kein Fehler. */
|
||||
profil: reihe || Object.fromEntries(felder.map((f) => [f, null])),
|
||||
darf_alles: recht === "voll",
|
||||
});
|
||||
} catch (fehler) {
|
||||
console.error("[workspace] Profil lesen:", fehler?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
/* Liste aller Creator -- fürs Management, zur Auswahl. */
|
||||
profilRouter.get("/workspace/api/profil", (req, res) => {
|
||||
try {
|
||||
if (req.person.rolle === "creator") {
|
||||
return res.json({ creator: [{ id: req.person.id, name: req.person.name }], eigen: true });
|
||||
}
|
||||
if (req.person.rolle !== "admin") return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
res.json({
|
||||
creator: db().prepare(`
|
||||
SELECT p.id, p.name, p.aktiv, p.letzter_login,
|
||||
(SELECT COUNT(*) FROM aufgaben a
|
||||
WHERE a.creator_id = p.id AND a.status <> 'erledigt') AS offene_aufgaben,
|
||||
(SELECT naechster_review FROM profile f WHERE f.person_id = p.id) AS naechster_review,
|
||||
(SELECT geaendert FROM profile f WHERE f.person_id = p.id) AS profil_geaendert
|
||||
FROM personen p WHERE p.rolle = 'creator'
|
||||
ORDER BY p.aktiv DESC, p.name`).all(),
|
||||
eigen: false,
|
||||
});
|
||||
} catch (fehler) {
|
||||
console.error("[workspace] Creator-Liste:", fehler?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
/* ---------- Speichern ---------------------------------------------------- */
|
||||
|
||||
profilRouter.put("/workspace/api/profil/:id", gleicheHerkunft, (req, res) => {
|
||||
try {
|
||||
const id = Number(req.params.id);
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ fehler: "ungueltig" });
|
||||
|
||||
const recht = darf(req.person, id);
|
||||
if (!recht) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
|
||||
const person = db().prepare("SELECT id, rolle FROM personen WHERE id = ?").get(id);
|
||||
if (!person || person.rolle !== "creator") return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
|
||||
/* Ein Creator kann die Verwaltungsfelder nicht setzen -- sie stehen
|
||||
gar nicht erst in der Liste der zulässigen Felder. Was er trotzdem
|
||||
mitschickt, wird stillschweigend ignoriert. */
|
||||
const erlaubt = recht === "voll" ? [...FELDER_OFFEN, ...FELDER_ADMIN] : FELDER_OFFEN;
|
||||
|
||||
const werte = {};
|
||||
for (const feld of erlaubt) {
|
||||
if (req.body?.[feld] === undefined) continue;
|
||||
let wert = req.body[feld];
|
||||
if (wert === null) { werte[feld] = null; continue; }
|
||||
wert = String(wert).trim();
|
||||
if (!wert) { werte[feld] = null; continue; }
|
||||
if (DATUMSFELDER.has(feld)) {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(wert) || Number.isNaN(Date.parse(wert))) {
|
||||
return res.status(400).json({ fehler: `Kein gültiges Datum: ${feld}` });
|
||||
}
|
||||
} else if (wert.length > TEXT_MAX) {
|
||||
return res.status(400).json({ fehler: "Ein Feld ist zu lang." });
|
||||
}
|
||||
werte[feld] = wert;
|
||||
}
|
||||
if (!Object.keys(werte).length) return res.status(400).json({ fehler: "nichts_zu_speichern" });
|
||||
|
||||
const spalten = Object.keys(werte);
|
||||
/* UPSERT: Beim ersten Speichern entsteht die Zeile, danach wird sie
|
||||
aktualisiert -- ohne vorher prüfen zu müssen, ob es sie gibt. */
|
||||
db().prepare(`
|
||||
INSERT INTO profile (person_id, ${spalten.join(", ")}, geaendert, geaendert_von)
|
||||
VALUES (?, ${spalten.map(() => "?").join(", ")}, ?, ?)
|
||||
ON CONFLICT(person_id) DO UPDATE SET
|
||||
${spalten.map((s) => `${s} = excluded.${s}`).join(", ")},
|
||||
geaendert = excluded.geaendert,
|
||||
geaendert_von = excluded.geaendert_von
|
||||
`).run(id, ...spalten.map((s) => werte[s]), jetzt(), req.person.id);
|
||||
|
||||
protokolliere("profil_gespeichert", {
|
||||
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
||||
/* Nur die Feldnamen, nie die Inhalte -- im Profil stehen persönliche
|
||||
Angaben, die nicht zusätzlich im Protokoll landen sollen. */
|
||||
detail: `Creator #${id}: ${spalten.join(",")}`.slice(0, 120),
|
||||
});
|
||||
|
||||
res.json({ ok: true });
|
||||
} catch (fehler) {
|
||||
console.error("[workspace] Profil speichern:", fehler?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
+30
-1
@@ -125,6 +125,30 @@ export function db() {
|
||||
CREATE INDEX IF NOT EXISTS idx_aufgaben_status ON aufgaben (status);
|
||||
CREATE INDEX IF NOT EXISTS idx_aufgaben_creator ON aufgaben (creator_id);
|
||||
|
||||
/* Creator-Profil (Onboarding aus dem Konzept). Eine Zeile je
|
||||
Creator, entsteht erst beim ersten Speichern.
|
||||
admin_notiz ist bewusst Teil dieser Tabelle, wird aber nur an
|
||||
das Management ausgeliefert -- siehe workspace-profil.js. */
|
||||
CREATE TABLE IF NOT EXISTS profile (
|
||||
person_id INTEGER PRIMARY KEY REFERENCES personen(id) ON DELETE CASCADE,
|
||||
handles TEXT,
|
||||
nische TEXT,
|
||||
live_zeiten TEXT,
|
||||
technik TEXT,
|
||||
ziel_live TEXT,
|
||||
ziel_content TEXT,
|
||||
ziel_community TEXT,
|
||||
ziel_technik TEXT,
|
||||
plan_start TEXT,
|
||||
plan_prio1 TEXT,
|
||||
plan_prio2 TEXT,
|
||||
plan_prio3 TEXT,
|
||||
naechster_review TEXT,
|
||||
admin_notiz TEXT,
|
||||
geaendert TEXT,
|
||||
geaendert_von INTEGER REFERENCES personen(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS versuche (
|
||||
ip TEXT NOT NULL,
|
||||
zeitpunkt TEXT NOT NULL
|
||||
@@ -257,6 +281,7 @@ const GESCHUETZT = {
|
||||
"/workspace/start.html": null,
|
||||
"/workspace/aufgaben.html": null,
|
||||
"/workspace/personen.html": ["admin"],
|
||||
"/workspace/profil.html": ["admin", "creator"],
|
||||
};
|
||||
|
||||
workspaceRouter.use((req, res, next) => {
|
||||
@@ -335,7 +360,11 @@ workspaceRouter.post("/workspace/api/abmelden", (req, res) => {
|
||||
workspaceRouter.get("/workspace/api/ich", (req, res) => {
|
||||
const person = sitzungLesen(req);
|
||||
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
|
||||
res.json({ name: person.name, rolle: person.rolle });
|
||||
/* Die eigene Nummer gehört mit dazu: Ohne sie kann die Oberfläche nicht
|
||||
erkennen, welcher Eintrag der eigene ist (etwa "das bin ich" in der
|
||||
Personenliste), und das eigene Profil liesse sich gar nicht aufrufen.
|
||||
Ein Geheimnis ist sie nicht -- sie beschreibt nur den Angemeldeten. */
|
||||
res.json({ id: person.id, name: person.name, rolle: person.rolle });
|
||||
});
|
||||
|
||||
/* ---------- Verwaltung (nur über die Kommandozeile) --------------------
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/* ===================================================================
|
||||
Creator-Profil (Onboarding).
|
||||
=================================================================== */
|
||||
|
||||
.gruppe {
|
||||
margin: 0 0 22px;
|
||||
padding: 20px 22px 22px;
|
||||
background: rgba(255, 255, 255, .03);
|
||||
border: 1px solid var(--rand);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.gruppe__titel {
|
||||
padding: 0 6px;
|
||||
font-size: .96rem; font-weight: 700;
|
||||
color: var(--text);
|
||||
}
|
||||
.gruppe__text { margin: 2px 0 18px; font-size: .86rem; color: var(--text-still); }
|
||||
|
||||
.gruppe .feld-schild { margin-top: 14px; }
|
||||
.gruppe .feld-schild:first-of-type { margin-top: 0; }
|
||||
|
||||
.gruppe textarea, .gruppe input {
|
||||
width: 100%; padding: 10px 12px;
|
||||
background: rgba(255, 255, 255, .045);
|
||||
border: 1px solid var(--rand);
|
||||
border-radius: var(--radius-klein);
|
||||
color: var(--text); font: inherit;
|
||||
transition: border-color var(--tempo), box-shadow var(--tempo);
|
||||
}
|
||||
.gruppe textarea { resize: vertical; min-height: 46px; }
|
||||
.gruppe textarea:focus, .gruppe input:focus {
|
||||
outline: none; border-color: rgba(63, 189, 245, .7);
|
||||
box-shadow: 0 0 0 3px rgba(63, 189, 245, .16);
|
||||
}
|
||||
.gruppe input[type="date"]::-webkit-calendar-picker-indicator { filter: invert(.75); cursor: pointer; }
|
||||
|
||||
.gruppe__raster {
|
||||
display: grid; gap: 14px; margin-top: 16px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
|
||||
}
|
||||
|
||||
/* Die interne Notiz hebt sich deutlich ab, damit im Gespräch mit dem
|
||||
Creator niemand versehentlich hineinschreibt, was er lesen soll. */
|
||||
.gruppe--intern {
|
||||
border-color: rgba(224, 163, 60, .38);
|
||||
background: rgba(224, 163, 60, .05);
|
||||
}
|
||||
.gruppe--intern .gruppe__titel { color: #e6b45c; }
|
||||
|
||||
#auswahl-block select {
|
||||
min-width: 210px; padding: 10px 12px;
|
||||
background: rgba(255, 255, 255, .045);
|
||||
border: 1px solid var(--rand); border-radius: var(--radius-klein);
|
||||
color: var(--text); font: inherit;
|
||||
}
|
||||
|
||||
.speicher-leiste {
|
||||
position: sticky; bottom: 0;
|
||||
display: flex; align-items: center; gap: 14px;
|
||||
margin-top: 26px; padding: 14px 0;
|
||||
background: linear-gradient(180deg, transparent, var(--tinte) 34%);
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.gruppe { padding: 16px 15px 18px; }
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/* ===================================================================
|
||||
Creator-Profil.
|
||||
|
||||
Welche Felder überhaupt ankommen, entscheidet der Server anhand der
|
||||
Rolle -- `admin_notiz` fehlt in der Antwort an einen Creator komplett.
|
||||
Diese Datei blendet deshalb nur aus, was ohnehin nicht da ist.
|
||||
=================================================================== */
|
||||
(() => {
|
||||
'use strict';
|
||||
|
||||
const OFFEN = ['handles', 'nische', 'live_zeiten', 'technik',
|
||||
'ziel_live', 'ziel_content', 'ziel_community', 'ziel_technik',
|
||||
'plan_prio1', 'plan_prio2', 'plan_prio3'];
|
||||
const NUR_ADMIN = ['plan_start', 'naechster_review', 'admin_notiz'];
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
let ich = null;
|
||||
let aktuell = null;
|
||||
let darfAlles = false;
|
||||
|
||||
const melde = (t) => { $('fehler').textContent = t || ''; };
|
||||
|
||||
async function hole(pfad, optionen = {}) {
|
||||
const a = await fetch(pfad, { credentials: 'same-origin', ...optionen });
|
||||
if (a.status === 401) { location.assign('/workspace/'); throw new Error('abgemeldet'); }
|
||||
return a;
|
||||
}
|
||||
|
||||
function fuelle(profil) {
|
||||
for (const f of [...OFFEN, ...NUR_ADMIN]) {
|
||||
const feld = $(f);
|
||||
if (!feld) continue;
|
||||
/* undefined heißt: Der Server hat das Feld nicht geschickt (fehlende
|
||||
Berechtigung). Dann bleibt es leer und wird auch nicht gesendet. */
|
||||
feld.value = profil[f] ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
function datumHuebsch(iso) {
|
||||
return iso.slice(0, 10).split('-').reverse().join('.');
|
||||
}
|
||||
|
||||
async function profilLaden(id) {
|
||||
melde('');
|
||||
try {
|
||||
const a = await hole('/workspace/api/profil/' + id);
|
||||
if (!a.ok) { melde('Profil konnte nicht geladen werden.'); return; }
|
||||
const daten = await a.json();
|
||||
aktuell = daten.person.id;
|
||||
darfAlles = daten.darf_alles;
|
||||
|
||||
$('titel').textContent = ich.rolle === 'admin'
|
||||
? 'Profil: ' + daten.person.name
|
||||
: 'Mein Profil';
|
||||
$('unterzeile').textContent = daten.profil.geaendert
|
||||
? 'Zuletzt geändert am ' + datumHuebsch(daten.profil.geaendert)
|
||||
: 'Noch nichts eingetragen.';
|
||||
|
||||
$('gruppe-intern').hidden = !darfAlles;
|
||||
$('termine').hidden = !darfAlles;
|
||||
fuelle(daten.profil);
|
||||
$('formular').hidden = false;
|
||||
} catch { /* umgeleitet */ }
|
||||
}
|
||||
|
||||
$('formular').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
melde('');
|
||||
const knopf = $('speichern');
|
||||
const daten = {};
|
||||
for (const f of darfAlles ? [...OFFEN, ...NUR_ADMIN] : OFFEN) {
|
||||
const feld = $(f);
|
||||
if (feld) daten[f] = feld.value;
|
||||
}
|
||||
knopf.disabled = true;
|
||||
$('stand').textContent = 'Speichere …';
|
||||
try {
|
||||
const a = await hole('/workspace/api/profil/' + aktuell, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(daten),
|
||||
});
|
||||
if (!a.ok) {
|
||||
melde((await a.json().catch(() => ({}))).fehler || 'Speichern ging nicht.');
|
||||
$('stand').textContent = '';
|
||||
return;
|
||||
}
|
||||
$('stand').textContent = 'Gespeichert ✓';
|
||||
setTimeout(() => { $('stand').textContent = ''; }, 2600);
|
||||
} catch { /* umgeleitet */ } finally { knopf.disabled = false; }
|
||||
});
|
||||
|
||||
$('abmelden').addEventListener('click', async () => {
|
||||
try { await fetch('/workspace/api/abmelden', { method: 'POST', credentials: 'same-origin' }); }
|
||||
catch { /* egal */ }
|
||||
location.assign('/workspace/');
|
||||
});
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const a = await hole('/workspace/api/ich');
|
||||
if (!a.ok) { location.assign('/workspace/'); return; }
|
||||
ich = await a.json();
|
||||
} catch { return; }
|
||||
if (ich.rolle === 'scout') { location.assign('/workspace/start.html'); return; }
|
||||
$('wer').textContent = ich.name + ' · ' + ich.rolle;
|
||||
|
||||
let liste;
|
||||
try { liste = await (await hole('/workspace/api/profil')).json(); } catch { return; }
|
||||
|
||||
if (ich.rolle === 'admin') {
|
||||
if (!liste.creator.length) {
|
||||
$('unterzeile').textContent =
|
||||
'Es gibt noch keinen Creator. Lege zuerst unter Personen & Zugänge einen an.';
|
||||
return;
|
||||
}
|
||||
$('auswahl-block').hidden = false;
|
||||
for (const c of liste.creator) {
|
||||
const o = document.createElement('option');
|
||||
o.value = String(c.id);
|
||||
o.textContent = c.name + (c.aktiv ? '' : ' (gesperrt)');
|
||||
$('auswahl').append(o);
|
||||
}
|
||||
$('auswahl').addEventListener('change', () => profilLaden(Number($('auswahl').value)));
|
||||
await profilLaden(liste.creator[0].id);
|
||||
} else {
|
||||
await profilLaden(ich.id);
|
||||
}
|
||||
})();
|
||||
})();
|
||||
@@ -18,8 +18,8 @@
|
||||
const BEREICHE = [
|
||||
{ name: 'Dashboard', phase: 1, rollen: ['admin', 'creator', 'scout'] },
|
||||
{ name: 'Personen & Zugänge', phase: 1, rollen: ['admin'], ziel: 'personen.html' },
|
||||
{ name: 'Creator-Profile', phase: 1, rollen: ['admin'] },
|
||||
{ name: 'Mein Profil', phase: 1, rollen: ['creator'] },
|
||||
{ name: 'Creator-Profile', phase: 1, rollen: ['admin'], ziel: 'profil.html' },
|
||||
{ name: 'Mein Profil', phase: 1, rollen: ['creator'], ziel: 'profil.html' },
|
||||
{ name: 'Aufgaben', phase: 1, rollen: ['admin', 'creator', 'scout'], ziel: 'aufgaben.html' },
|
||||
{ name: 'Kalender', phase: 1, rollen: ['admin', 'creator', 'scout'] },
|
||||
{ name: 'Dateien', phase: 1, rollen: ['admin', 'creator'] },
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<title>Creator-Profil · Creator Workspace</title>
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<meta name="theme-color" content="#05070d" />
|
||||
<link rel="icon" href="assets/img/favicon.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css" />
|
||||
<link rel="stylesheet" href="assets/css/start.css" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css" />
|
||||
<link rel="stylesheet" href="assets/css/profil.css" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
|
||||
<header class="kopfleiste">
|
||||
<p class="marke"><a class="zurueck" href="start.html">Creator Workspace</a> · Profil</p>
|
||||
<div class="kopfleiste__rechts">
|
||||
<span class="wer" id="wer">…</span>
|
||||
<button type="button" class="abmelden" id="abmelden">Abmelden</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="inhalt">
|
||||
|
||||
<section class="kopf-zeile">
|
||||
<div>
|
||||
<p class="marke">Onboarding</p>
|
||||
<h1 class="titel" id="titel">Creator-Profil</h1>
|
||||
<p class="unterzeile" id="unterzeile">…</p>
|
||||
</div>
|
||||
<div id="auswahl-block" hidden>
|
||||
<label class="feld-schild" for="auswahl">Creator</label>
|
||||
<select id="auswahl"></select>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<p class="fehler" id="fehler" role="alert" aria-live="polite"></p>
|
||||
|
||||
<form id="formular" hidden>
|
||||
|
||||
<fieldset class="gruppe">
|
||||
<legend class="gruppe__titel">Stammdaten</legend>
|
||||
<p class="gruppe__text">Wer ist der Creator, und wie arbeitet er?</p>
|
||||
|
||||
<label class="feld-schild" for="handles">Social-Handles</label>
|
||||
<textarea id="handles" rows="2" maxlength="2000"
|
||||
placeholder="TikTok, Instagram, Discord …"></textarea>
|
||||
|
||||
<label class="feld-schild" for="nische">Nische & Content-Stil</label>
|
||||
<textarea id="nische" rows="2" maxlength="2000"
|
||||
placeholder="Worum geht es, und wie wirkt es?"></textarea>
|
||||
|
||||
<label class="feld-schild" for="live_zeiten">LIVE-Zeiten & Routinen</label>
|
||||
<textarea id="live_zeiten" rows="2" maxlength="2000"
|
||||
placeholder="Wann wird gestreamt, wie regelmäßig?"></textarea>
|
||||
|
||||
<label class="feld-schild" for="technik">Technik-Setup & Tools</label>
|
||||
<textarea id="technik" rows="2" maxlength="2000"
|
||||
placeholder="PC/Handy, Kamera, Mikro, OBS …"></textarea>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="gruppe">
|
||||
<legend class="gruppe__titel">Ziele</legend>
|
||||
<p class="gruppe__text">Was soll wachsen? Aus dem Konzept, Seite 5.</p>
|
||||
|
||||
<label class="feld-schild" for="ziel_live">LIVE-Qualität & Bindung</label>
|
||||
<textarea id="ziel_live" rows="2" maxlength="2000"></textarea>
|
||||
|
||||
<label class="feld-schild" for="ziel_content">Content-Reichweite</label>
|
||||
<textarea id="ziel_content" rows="2" maxlength="2000"></textarea>
|
||||
|
||||
<label class="feld-schild" for="ziel_community">Community & Wiederkehr</label>
|
||||
<textarea id="ziel_community" rows="2" maxlength="2000"></textarea>
|
||||
|
||||
<label class="feld-schild" for="ziel_technik">Technische Stabilität</label>
|
||||
<textarea id="ziel_technik" rows="2" maxlength="2000"></textarea>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="gruppe">
|
||||
<legend class="gruppe__titel">90-Tage-Plan</legend>
|
||||
<p class="gruppe__text">Wenige Punkte, dafür verbindlich – Top 3 aus dem Konzept.</p>
|
||||
|
||||
<label class="feld-schild" for="plan_prio1">Priorität 1</label>
|
||||
<input id="plan_prio1" maxlength="2000" />
|
||||
<label class="feld-schild" for="plan_prio2">Priorität 2</label>
|
||||
<input id="plan_prio2" maxlength="2000" />
|
||||
<label class="feld-schild" for="plan_prio3">Priorität 3</label>
|
||||
<input id="plan_prio3" maxlength="2000" />
|
||||
|
||||
<div class="gruppe__raster" id="termine" hidden>
|
||||
<div>
|
||||
<label class="feld-schild" for="plan_start">Plan-Start</label>
|
||||
<input id="plan_start" type="date" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="feld-schild" for="naechster_review">Nächster Review</label>
|
||||
<input id="naechster_review" type="date" />
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- Nur fürs Management. Der Server schickt dieses Feld an Creator
|
||||
gar nicht erst mit. -->
|
||||
<fieldset class="gruppe gruppe--intern" id="gruppe-intern" hidden>
|
||||
<legend class="gruppe__titel">Interne Notiz</legend>
|
||||
<p class="gruppe__text">
|
||||
Sieht <strong>nur das Management</strong>. Der Creator bekommt dieses Feld
|
||||
weder angezeigt noch übertragen.
|
||||
</p>
|
||||
<textarea id="admin_notiz" rows="4" maxlength="2000"></textarea>
|
||||
</fieldset>
|
||||
|
||||
<div class="speicher-leiste">
|
||||
<button type="submit" class="knopf knopf--klein" id="speichern">Speichern</button>
|
||||
<span class="leise" id="stand"></span>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/profil.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user