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:
@@ -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