Auf Wunsch von Filipe. Betrifft ausschliesslich die Anzeige.
Der Rollenschluessel bleibt "admin". Er steckt in der CHECK-Regel der
Datenbank, in jeder bestehenden Sitzung und in jeder Rechteabfrage --
ihn umzubenennen haette alle drei gebrochen, und bestehende Anmeldungen
waeren ungueltig geworden. Umbenannt wird nur, was man LIEST.
Der Anzeigename steht jetzt an EINER Stelle (ROLLEN_NAME in
workspace.js) und kommt ueber /workspace/api/ich als rolle_name mit.
Stand er in elf Dateien, waere er beim naechsten Mal in zehn davon
geaendert.
Dabei aufgefallen: In der Kopfleiste stand auf JEDER Seite der rohe
Rollenschluessel -- "Chef · admin". Das war schon vorher unschoen, faellt
aber jetzt erst richtig auf. Alle elf Seiten zeigen jetzt "Chef ·
DogFather". Geprueft: kein rohes "admin" mehr in der Oberflaeche.
Geaendert: Rollenkachel und Untertitel auf der Anmeldeseite, Kopfleiste
aller Seiten, Rollentext auf dem Dashboard, Rollenmarken und Auswahl in
der Personenverwaltung, die Betreuungs-Auswahl ("nur DogFather"), der
Hinweis auf der Dateienseite, die Erklaerung zur internen Notiz im
Profil, die Hinweise fuer Scouts ohne Zuteilung -- und vier
Fehlermeldungen vom Server.
Schreibweise "DogFather" wie von Filipe geschrieben; das ist auch auf
der oeffentlichen Website die haeufigste Form (717 von 1216).
In den Code-Kommentaren der Fachmodule heisst die Rolle weiterhin "das
Management". Das bleibt bewusst so -- eine Massenaenderung an vierzig
Kommentaren waere reines Risiko ohne sichtbaren Nutzen. Ein Hinweis an
der ROLLEN_NAME-Zuordnung erklaert den Zusammenhang.
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_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. DogFather 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();
|
||
})();
|
||
})();
|