Letztes unumgesetztes Stueck aus dem Konzept, Seite 5, Block 03. Profil,
Ziele und 90-Tage-Plan gab es schon -- die Erstanalyse dazwischen fehlte.
"Die Betreuung startet strukturiert: Ausgangslage verstehen, Ziele
festlegen und daraus konkrete Arbeitspakete bauen."
Der letzte Halbsatz ist der entscheidende. Ein Check, der mit einer Note
endet, ist eine Beurteilung. Ein Check, der mit Aufgaben endet, ist
Betreuung. Jeder Punkt mit Handlungsbedarf bietet deshalb direkt an,
daraus eine Aufgabe zu machen -- mit dem Befund als Herkunft und
automatisch hoher Prioritaet. Dieselbe Linie wie beim Review ("endet mit
einer Entscheidung") und beim Call ("endet mit To-dos").
Die vier Felder aus dem Deck, mit je vier Punkten: Profil & Auftritt,
LIVE-Struktur, Content-Muster, Community & Modis. Drei Stufen: Passt,
Ausbaufaehig, Handlungsbedarf.
Die Punkte stehen fest im Code, NICHT in der Datenbank. Eine
Erstanalyse, bei der jeder eigene Punkte anlegt, ist keine Erstanalyse
mehr -- man koennte zwei Creator nicht mehr vergleichen, und genau das
ist ihr Zweck.
Sichtbarkeit, bewusst anders als beim Profil:
- LESEN darf auch der Creator selbst. Der Check ist Teil SEINES
Entwicklungsplans ("jeder Creator bekommt einen eigenen, lebenden
Entwicklungsplan"), kein Urteil hinter seinem Ruecken. Wer etwas
festhalten will, das er nicht sehen soll, hat dafuer die interne
Notiz im Profil -- die bleibt beim Management.
- AENDERN duerfen nur Management und zustaendiger Scout. Es ist eine
Fremdeinschaetzung; koennte der Creator sie selbst setzen, waere es
eine Selbsteinschaetzung. Geprueft: Luna bekommt 403 beim Bewerten und
beim Anlegen einer Aufgabe, sieht aber ihren Check inklusive Notizen.
- Ein nicht zustaendiger Scout bekommt 404, nicht 403.
Kein Speichern-Knopf: Jede Bewertung und jede Notiz wird sofort
gesichert. Eine Erstanalyse geht man im Gespraech durch -- da will
niemand am Ende noch an einen Knopf denken.
Ein leergeraeumter Punkt (keine Bewertung, keine Notiz) wird geloescht
statt als leere Zeile stehen zu bleiben. So sagt der Bestand direkt, wie
weit die Analyse ist.
Zwei neue Hinweise auf dem Dashboard:
- "Creator ohne Start-Check" -- die Sorte Luecke, die sonst niemandem
auffaellt, weil ja nichts fehlt: es fing nur nie an.
- "Punkt im Start-Check braucht Handlung", sichtbar fuer alle, die den
Check auch sehen duerfen.
Aus dem Test: Sechzehn dauerhaft offene Notizfelder machten die Seite
7026 px lang. Das Feld erscheint jetzt erst mit der Bewertung -- vorher
hat man ohnehin nichts zu notieren. 2460 px.
290 lines
10 KiB
JavaScript
290 lines
10 KiB
JavaScript
/* ===================================================================
|
||
Start-Check / Erstanalyse (Konzept, Seite 5).
|
||
|
||
Bewusst ohne Speichern-Knopf: Jede Bewertung wird sofort gesichert.
|
||
Eine Erstanalyse geht man im Gespraech durch -- da will niemand am
|
||
Ende noch daran denken, unten auf einen Knopf zu druecken.
|
||
|
||
Und: Jeder Punkt mit Handlungsbedarf bietet direkt an, daraus eine
|
||
Aufgabe zu machen. "Ausgangslage verstehen UND daraus konkrete
|
||
Arbeitspakete bauen" -- der zweite Teil ist der, der zaehlt.
|
||
=================================================================== */
|
||
(() => {
|
||
'use strict';
|
||
|
||
const $ = (id) => document.getElementById(id);
|
||
const STUFEN = [
|
||
{ wert: 'ok', name: 'Passt' },
|
||
{ wert: 'mittel', name: 'Ausbaufähig' },
|
||
{ wert: 'handlung', name: 'Handlungsbedarf' },
|
||
];
|
||
|
||
let ich = null;
|
||
let daten = null;
|
||
let aktuell = null;
|
||
|
||
const el = (tag, klasse, text) => {
|
||
const k = document.createElement(tag);
|
||
if (klasse) k.className = klasse;
|
||
if (text !== undefined) k.textContent = text;
|
||
return k;
|
||
};
|
||
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;
|
||
}
|
||
|
||
async function setze(feld, punkt, aenderung) {
|
||
try {
|
||
const a = await hole('/workspace/api/startcheck/' + aktuell, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ feld, punkt, ...aenderung }),
|
||
});
|
||
if (!a.ok) {
|
||
melde((await a.json().catch(() => ({}))).fehler || 'Konnte nicht gespeichert werden.');
|
||
return false;
|
||
}
|
||
melde('');
|
||
return true;
|
||
} catch { return false; }
|
||
}
|
||
|
||
/* ---------- Ein Punkt ---------------------------------------------------- */
|
||
|
||
function punktZeile(feld, p) {
|
||
const k = el('div', 'punkt');
|
||
if (p.bewertung) k.dataset.bewertung = p.bewertung;
|
||
|
||
const kopf = el('div', 'punkt__kopf');
|
||
const links = el('div');
|
||
links.append(el('h3', 'punkt__name', p.name));
|
||
links.append(el('p', 'punkt__hinweis', p.hinweis));
|
||
kopf.append(links);
|
||
|
||
/* Bewertung */
|
||
if (daten.darf_aendern) {
|
||
const wahl = el('div', 'stufen');
|
||
wahl.setAttribute('role', 'group');
|
||
wahl.setAttribute('aria-label', 'Bewertung für ' + p.name);
|
||
for (const s of STUFEN) {
|
||
const b = el('button', 'stufe-knopf', s.name);
|
||
b.type = 'button';
|
||
b.dataset.wert = s.wert;
|
||
if (p.bewertung === s.wert) b.dataset.aktiv = 'ja';
|
||
b.addEventListener('click', async () => {
|
||
/* Nochmal auf dieselbe Stufe = zurücknehmen. */
|
||
const neu = p.bewertung === s.wert ? null : s.wert;
|
||
if (await setze(feld.schluessel, p.schluessel, { bewertung: neu, notiz: p.notiz })) {
|
||
await laden();
|
||
}
|
||
});
|
||
wahl.append(b);
|
||
}
|
||
kopf.append(wahl);
|
||
} else if (p.bewertung) {
|
||
const m = el('span', 'stufe-marke', STUFEN.find((s) => s.wert === p.bewertung)?.name);
|
||
m.dataset.wert = p.bewertung;
|
||
kopf.append(m);
|
||
}
|
||
k.append(kopf);
|
||
|
||
/* Notiz erst, wenn bewertet wurde. Sechzehn offene Textfelder
|
||
machen aus einer Checkliste eine Wand -- und vor der Bewertung
|
||
hat man ohnehin nichts zu notieren. */
|
||
if (daten.darf_aendern && (p.bewertung || p.notiz)) {
|
||
const n = el('textarea', 'punkt__notiz');
|
||
n.rows = 2;
|
||
n.maxLength = 2000;
|
||
n.placeholder = 'Was ist dir aufgefallen?';
|
||
n.value = p.notiz || '';
|
||
let warte = null;
|
||
n.addEventListener('input', () => {
|
||
clearTimeout(warte);
|
||
warte = setTimeout(() => {
|
||
setze(feld.schluessel, p.schluessel, { bewertung: p.bewertung, notiz: n.value });
|
||
p.notiz = n.value;
|
||
}, 700);
|
||
});
|
||
k.append(n);
|
||
} else if (p.notiz) {
|
||
k.append(el('p', 'punkt__notiz-lesen', p.notiz));
|
||
}
|
||
|
||
/* Aus dem Befund ein Arbeitspaket machen. */
|
||
if (daten.darf_aendern && (p.bewertung === 'handlung' || p.bewertung === 'mittel')) {
|
||
k.append(aufgabenTeil(feld, p));
|
||
}
|
||
|
||
if (p.von && p.geaendert) {
|
||
k.append(el('p', 'punkt__quelle',
|
||
`${p.von} · ${p.geaendert.slice(0, 10).split('-').reverse().join('.')}`));
|
||
}
|
||
return k;
|
||
}
|
||
|
||
function aufgabenTeil(feld, p) {
|
||
const box = el('div', 'paket');
|
||
const auf = el('button', 'knopf-still', '+ Daraus eine Aufgabe machen');
|
||
auf.type = 'button';
|
||
|
||
const form = el('form', 'paket__form');
|
||
form.hidden = true;
|
||
const titel = el('input', 'paket__titel');
|
||
titel.type = 'text';
|
||
titel.maxLength = 160;
|
||
titel.placeholder = 'Was genau ist zu tun?';
|
||
const frist = el('input', 'paket__frist');
|
||
frist.type = 'date';
|
||
frist.title = 'Frist (optional)';
|
||
const ok = el('button', 'knopf knopf--klein', 'Anlegen');
|
||
ok.type = 'submit';
|
||
form.append(titel, frist, ok);
|
||
|
||
const stand = el('span', 'leise');
|
||
|
||
auf.addEventListener('click', () => {
|
||
form.hidden = false;
|
||
auf.hidden = true;
|
||
/* Ein sinnvoller Vorschlag statt eines leeren Feldes -- die Notiz
|
||
ist meistens schon die halbe Aufgabe. */
|
||
titel.value = p.notiz ? p.notiz.split(/[.\n]/)[0].trim().slice(0, 160) : p.name;
|
||
titel.focus();
|
||
titel.select();
|
||
});
|
||
|
||
form.addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
ok.disabled = true;
|
||
try {
|
||
const a = await hole(`/workspace/api/startcheck/${aktuell}/aufgabe`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
feld: feld.schluessel, punkt: p.schluessel,
|
||
titel: titel.value, frist: frist.value,
|
||
}),
|
||
});
|
||
if (!a.ok) {
|
||
melde((await a.json().catch(() => ({}))).fehler || 'Ging nicht.');
|
||
return;
|
||
}
|
||
const r = await a.json();
|
||
form.hidden = true;
|
||
stand.textContent = `Im Aufgabenbrett angelegt (Priorität ${r.prioritaet}) ✓`;
|
||
setTimeout(() => { stand.textContent = ''; auf.hidden = false; }, 4000);
|
||
} catch { /* umgeleitet */ } finally { ok.disabled = false; }
|
||
});
|
||
|
||
box.append(auf, form, stand);
|
||
return box;
|
||
}
|
||
|
||
/* ---------- Zeichnen ----------------------------------------------------- */
|
||
|
||
function zeichne() {
|
||
const ziel = $('felder');
|
||
ziel.textContent = '';
|
||
ziel.setAttribute('aria-busy', 'false');
|
||
|
||
for (const f of daten.felder) {
|
||
const s = el('section', 'feld-block');
|
||
const kopf = el('div', 'feld-block__kopf');
|
||
kopf.append(el('h2', 'feld-block__titel', f.name));
|
||
const fertig = f.punkte.filter((p) => p.bewertung).length;
|
||
const z = el('span', 'feld-block__zahl', `${fertig}/${f.punkte.length}`);
|
||
if (fertig === f.punkte.length) z.dataset.fertig = 'ja';
|
||
kopf.append(z);
|
||
s.append(kopf);
|
||
const box = el('div', 'feld-block__punkte');
|
||
for (const p of f.punkte) box.append(punktZeile(f, p));
|
||
s.append(box);
|
||
ziel.append(s);
|
||
}
|
||
|
||
/* Fortschritt */
|
||
$('fortschritt').hidden = false;
|
||
$('f-zahl').textContent = `${daten.geprueft} von ${daten.gesamt}`;
|
||
const anteil = Math.round((daten.geprueft / daten.gesamt) * 100);
|
||
$('f-balken').style.width = anteil + '%';
|
||
const aussen = $('f-balken-aussen');
|
||
aussen.setAttribute('aria-valuenow', String(daten.geprueft));
|
||
aussen.setAttribute('aria-valuemax', String(daten.gesamt));
|
||
|
||
const h = $('f-hinweis');
|
||
if (daten.geprueft === 0) {
|
||
h.textContent = daten.darf_aendern
|
||
? 'Noch nichts geprüft. Am besten im Gespräch durchgehen – gespeichert wird sofort.'
|
||
: 'Die Erstanalyse wurde noch nicht ausgefüllt.';
|
||
} else if (daten.handlungsbedarf) {
|
||
h.textContent = `${daten.handlungsbedarf} ${daten.handlungsbedarf === 1
|
||
? 'Punkt braucht' : 'Punkte brauchen'} Handlung`
|
||
+ (daten.darf_aendern ? ' – daraus lassen sich direkt Aufgaben machen.' : '.');
|
||
} else if (daten.geprueft === daten.gesamt) {
|
||
h.textContent = 'Vollständig geprüft, nichts Dringendes offen.';
|
||
} else {
|
||
h.textContent = '';
|
||
}
|
||
$('fortschritt').dataset.stufe = daten.handlungsbedarf ? 'handlung'
|
||
: daten.geprueft === daten.gesamt ? 'fertig' : 'laeuft';
|
||
}
|
||
|
||
async function laden() {
|
||
try {
|
||
const a = await hole('/workspace/api/startcheck/' + aktuell);
|
||
if (a.status === 404) {
|
||
$('felder').textContent = '';
|
||
$('felder').append(el('p', 'leer-hinweis', 'Dieser Start-Check ist für dich nicht sichtbar.'));
|
||
return;
|
||
}
|
||
if (!a.ok) { melde('Start-Check konnte nicht geladen werden.'); return; }
|
||
daten = await a.json();
|
||
zeichne();
|
||
} catch { /* umgeleitet */ }
|
||
}
|
||
|
||
/* ---------- Start --------------------------------------------------------- */
|
||
|
||
(async () => {
|
||
try {
|
||
const a = await hole('/workspace/api/ich');
|
||
if (!a.ok) { location.assign('/workspace/'); return; }
|
||
ich = await a.json();
|
||
} catch { return; }
|
||
$('wer').textContent = ich.name + ' · ' + ich.rolle;
|
||
|
||
let liste;
|
||
try { liste = await (await hole('/workspace/api/startcheck/creator')).json(); } catch { return; }
|
||
|
||
if (!liste.creator.length) {
|
||
$('felder').textContent = '';
|
||
$('felder').append(el('p', 'leer-hinweis', ich.rolle === 'scout'
|
||
? 'Dir ist noch kein Creator zugeteilt. Das Management legt fest, wen du betreust.'
|
||
: 'Es gibt noch keinen Creator.'));
|
||
return;
|
||
}
|
||
|
||
if (!liste.eigen) {
|
||
$('creator-block').hidden = false;
|
||
for (const c of liste.creator) {
|
||
const o = document.createElement('option');
|
||
o.value = String(c.id); o.textContent = c.name;
|
||
$('f-creator').append(o);
|
||
}
|
||
$('f-creator').addEventListener('change', () => {
|
||
aktuell = Number($('f-creator').value);
|
||
laden();
|
||
});
|
||
} else {
|
||
$('unterzeile').textContent =
|
||
'Die Erstanalyse zu deinem Kanal – Grundlage für deinen 90-Tage-Plan.';
|
||
}
|
||
|
||
aktuell = liste.creator[0].id;
|
||
await laden();
|
||
})();
|
||
})();
|