Aufgaben: - Bearbeiten-Dialog (Titel, Beschreibung, Prioritaet, Frist, Zuordnung). Als natives <dialog>: Fokusfang, Esc zum Schliessen und Abdunklung ohne eigenen Code. - Loeschen nur fuer Management, mit Rueckfrage und Protokolleintrag. Das Konzept will, dass Erledigtes stehen bleibt -- Loeschen ist der Ausnahmefall fuer Fehleintraege, nicht der normale Abschluss. Personen (/workspace/personen.html, nur Management): - Anlegen, Code erneuern, sperren/entsperren, Protokollansicht - Der Code wird genau einmal in der Antwort zurueckgegeben, nie gespeichert; beim Schliessen auch aus dem Dokument entfernt - Selbstschutz: niemand kann sich selbst sperren, und das letzte aktive Management laesst sich nicht sperren -- sonst kaeme niemand mehr hinein - Code fuer sich selbst tauschen nur mit ausdruecklicher Bestaetigung, weil es die eigene Sitzung sofort beendet Zwei Fehler, die beim Testen aufgefallen sind: 1. Rollenpruefung fehlte beim Ausliefern der Seiten. Ein Creator bekam personen.html mit HTTP 200 -- die Schnittstellen wiesen ihn zwar ab, das Geruest der Seite war aber sichtbar. GESCHUETZT ist jetzt eine Zuordnung Pfad -> erlaubte Rollen statt einer blossen Liste. 2. Das Protokoll nannte den falschen Verursacher. personAnlegen trug die NEU ANGELEGTE Person als person_id ein, der Eintrag las sich also so, als haette sie sich selbst angelegt. Akteur und Betroffener sind jetzt getrennt: Akteur in person_id, Betroffener im Text. Ueber die Kommandozeile angelegte Personen zeigen korrekt keinen Akteur.
290 lines
9.8 KiB
JavaScript
290 lines
9.8 KiB
JavaScript
/* ===================================================================
|
|
Aufgabenbrett.
|
|
|
|
Der Server entscheidet, wer was sehen und ändern darf -- diese Datei
|
|
zeigt nur an. Was hier ausgeblendet wird, ist Bequemlichkeit, kein
|
|
Schutz.
|
|
=================================================================== */
|
|
(() => {
|
|
'use strict';
|
|
|
|
const SPALTEN = [
|
|
{ status: 'offen', name: 'Offen' },
|
|
{ status: 'arbeit', name: 'In Arbeit' },
|
|
{ status: 'review', name: 'Review' },
|
|
{ status: 'erledigt', name: 'Erledigt' },
|
|
];
|
|
const WEITER = { offen: 'arbeit', arbeit: 'review', review: 'erledigt' };
|
|
const ZURUECK = { arbeit: 'offen', review: 'arbeit', erledigt: 'review' };
|
|
|
|
const $ = (id) => document.getElementById(id);
|
|
const heute = new Date().toISOString().slice(0, 10);
|
|
let ich = null;
|
|
|
|
/* Texte werden ausschließlich über textContent gesetzt, nie über
|
|
innerHTML -- ein Aufgabentitel darf keine Auszeichnung einschleusen. */
|
|
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 = (text) => { $('fehler').textContent = text || ''; };
|
|
|
|
async function hole(pfad, optionen = {}) {
|
|
const antwort = await fetch(pfad, { credentials: 'same-origin', ...optionen });
|
|
if (antwort.status === 401) { location.assign('/workspace/'); throw new Error('abgemeldet'); }
|
|
return antwort;
|
|
}
|
|
|
|
/* ---------- Brett zeichnen ------------------------------------------ */
|
|
|
|
function datumHuebsch(iso) {
|
|
const [j, m, t] = iso.split('-');
|
|
return `${t}.${m}.${j.slice(2)}`;
|
|
}
|
|
|
|
function karte(a) {
|
|
const k = el('article', 'karte');
|
|
k.dataset.prio = a.prioritaet;
|
|
|
|
const kopf = el('div', 'karte__kopf');
|
|
kopf.append(el('h3', 'karte__titel', a.titel));
|
|
const stift = el('button', 'karte__stift', 'bearbeiten');
|
|
stift.type = 'button';
|
|
stift.setAttribute('aria-label', `${a.titel} bearbeiten`);
|
|
stift.addEventListener('click', () => dialogOeffnen(a));
|
|
kopf.append(stift);
|
|
k.append(kopf);
|
|
if (a.beschreibung) k.append(el('p', 'karte__text', a.beschreibung));
|
|
|
|
const zeile = el('div', 'karte__zeile');
|
|
if (a.frist) {
|
|
const faellig = a.status !== 'erledigt' && a.frist < heute;
|
|
const f = el('span', 'karte__frist' + (faellig ? ' karte__frist--faellig' : ''),
|
|
(faellig ? 'überfällig ' : 'bis ') + datumHuebsch(a.frist));
|
|
zeile.append(f);
|
|
}
|
|
if (a.verantwortlich_name) zeile.append(el('span', null, '· ' + a.verantwortlich_name));
|
|
if (ich.rolle === 'admin' && a.creator_name && a.creator_name !== a.verantwortlich_name) {
|
|
zeile.append(el('span', null, '· Bereich ' + a.creator_name));
|
|
}
|
|
if (zeile.childElementCount) k.append(zeile);
|
|
|
|
const schritte = el('div', 'karte__schritte');
|
|
if (ZURUECK[a.status]) schritte.append(knopf('◀ zurück', a, ZURUECK[a.status]));
|
|
if (WEITER[a.status]) schritte.append(knopf(beschriftung(a.status), a, WEITER[a.status]));
|
|
if (schritte.childElementCount) k.append(schritte);
|
|
return k;
|
|
}
|
|
|
|
const beschriftung = (status) => ({
|
|
offen: 'starten ▶', arbeit: 'zum Review ▶', review: 'erledigt ▶',
|
|
})[status];
|
|
|
|
function knopf(text, aufgabe, neuerStatus) {
|
|
const b = el('button', 'schritt', text);
|
|
b.type = 'button';
|
|
b.addEventListener('click', async () => {
|
|
b.disabled = true;
|
|
melde('');
|
|
try {
|
|
const antwort = await hole(`/workspace/api/aufgaben/${aufgabe.id}`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ status: neuerStatus }),
|
|
});
|
|
if (!antwort.ok) {
|
|
const d = await antwort.json().catch(() => ({}));
|
|
melde(d.fehler === 'nicht_erlaubt'
|
|
? 'Diese Aufgabe darfst du nicht ändern.'
|
|
: 'Ändern hat nicht geklappt.');
|
|
b.disabled = false;
|
|
return;
|
|
}
|
|
await laden();
|
|
} catch { b.disabled = false; }
|
|
});
|
|
return b;
|
|
}
|
|
|
|
function zeichne(aufgaben) {
|
|
const brett = $('brett');
|
|
brett.textContent = '';
|
|
brett.setAttribute('aria-busy', 'false');
|
|
|
|
for (const s of SPALTEN) {
|
|
const eigene = aufgaben.filter((a) => a.status === s.status);
|
|
const spalte = el('section', 'spalte');
|
|
spalte.dataset.status = s.status;
|
|
|
|
const kopf = el('h2', 'spalte__kopf');
|
|
kopf.append(el('span', 'spalte__punkt'), el('span', null, s.name),
|
|
el('span', 'spalte__zahl', String(eigene.length)));
|
|
spalte.append(kopf);
|
|
|
|
if (!eigene.length) spalte.append(el('p', 'spalte__leer', '—'));
|
|
else for (const a of eigene) spalte.append(karte(a));
|
|
|
|
brett.append(spalte);
|
|
}
|
|
}
|
|
|
|
async function laden() {
|
|
try {
|
|
const antwort = await hole('/workspace/api/aufgaben');
|
|
if (!antwort.ok) { melde('Aufgaben konnten nicht geladen werden.'); return; }
|
|
zeichne((await antwort.json()).aufgaben);
|
|
} catch { /* bereits umgeleitet */ }
|
|
}
|
|
|
|
/* ---------- Neue Aufgabe -------------------------------------------- */
|
|
|
|
function formularUmschalten(auf) {
|
|
$('neu').hidden = !auf;
|
|
$('neu-oeffnen').hidden = auf;
|
|
if (auf) $('f-titel').focus();
|
|
}
|
|
|
|
$('neu-oeffnen').addEventListener('click', () => formularUmschalten(true));
|
|
$('neu-abbrechen').addEventListener('click', () => {
|
|
$('neu').reset(); $('neu-fehler').textContent = ''; formularUmschalten(false);
|
|
});
|
|
|
|
$('neu').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const knopfEl = $('neu-speichern');
|
|
$('neu-fehler').textContent = '';
|
|
|
|
const daten = {
|
|
titel: $('f-titel').value,
|
|
beschreibung: $('f-text').value,
|
|
prioritaet: $('f-prio').value,
|
|
frist: $('f-frist').value,
|
|
};
|
|
if (ich.rolle === 'admin') {
|
|
daten.creator_id = $('f-creator').value || null;
|
|
daten.verantwortlich_id = $('f-verant').value || null;
|
|
}
|
|
|
|
knopfEl.disabled = true;
|
|
try {
|
|
const antwort = await hole('/workspace/api/aufgaben', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(daten),
|
|
});
|
|
if (!antwort.ok) {
|
|
const d = await antwort.json().catch(() => ({}));
|
|
$('neu-fehler').textContent = d.fehler || 'Anlegen hat nicht geklappt.';
|
|
return;
|
|
}
|
|
$('neu').reset();
|
|
formularUmschalten(false);
|
|
await laden();
|
|
} catch { /* bereits umgeleitet */ } finally { knopfEl.disabled = false; }
|
|
});
|
|
|
|
/* ---------- Bearbeiten ------------------------------------------------ */
|
|
|
|
const dialog = $('bearbeiten-dialog');
|
|
let bearbeitet = null;
|
|
|
|
function dialogOeffnen(a) {
|
|
bearbeitet = a;
|
|
$('b-titel').value = a.titel;
|
|
$('b-text').value = a.beschreibung || '';
|
|
$('b-prio').value = a.prioritaet;
|
|
$('b-frist').value = a.frist || '';
|
|
$('b-fehler').textContent = '';
|
|
if (ich.rolle === 'admin') {
|
|
$('b-creator').value = a.creator_id ? String(a.creator_id) : '';
|
|
$('b-verant').value = a.verantwortlich_id ? String(a.verantwortlich_id) : '';
|
|
$('b-loeschen').hidden = false;
|
|
}
|
|
dialog.showModal();
|
|
$('b-titel').focus();
|
|
}
|
|
|
|
$('b-abbrechen').addEventListener('click', () => dialog.close());
|
|
|
|
$('b-speichern').addEventListener('click', async () => {
|
|
const daten = {
|
|
titel: $('b-titel').value,
|
|
beschreibung: $('b-text').value,
|
|
prioritaet: $('b-prio').value,
|
|
frist: $('b-frist').value,
|
|
};
|
|
if (ich.rolle === 'admin') {
|
|
daten.creator_id = $('b-creator').value || null;
|
|
daten.verantwortlich_id = $('b-verant').value || null;
|
|
}
|
|
$('b-speichern').disabled = true;
|
|
try {
|
|
const a = await hole(`/workspace/api/aufgaben/${bearbeitet.id}`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(daten),
|
|
});
|
|
if (!a.ok) {
|
|
$('b-fehler').textContent = (await a.json().catch(() => ({}))).fehler || 'Speichern ging nicht.';
|
|
return;
|
|
}
|
|
dialog.close();
|
|
await laden();
|
|
} catch { /* umgeleitet */ } finally { $('b-speichern').disabled = false; }
|
|
});
|
|
|
|
$('b-loeschen').addEventListener('click', async () => {
|
|
if (!confirm(`Aufgabe wirklich loeschen?
|
|
|
|
"${bearbeitet.titel}"
|
|
|
|
Das laesst sich nicht rueckgaengig machen. Erledigte Aufgaben sollten normalerweise stehen bleiben.`)) return;
|
|
try {
|
|
const a = await hole(`/workspace/api/aufgaben/${bearbeitet.id}`, { method: 'DELETE' });
|
|
if (!a.ok) { $('b-fehler').textContent = 'Loeschen ging nicht.'; return; }
|
|
dialog.close();
|
|
await laden();
|
|
} catch { /* umgeleitet */ }
|
|
});
|
|
|
|
/* ---------- Start ----------------------------------------------------- */
|
|
|
|
$('abmelden').addEventListener('click', async () => {
|
|
try { await fetch('/workspace/api/abmelden', { method: 'POST', credentials: 'same-origin' }); }
|
|
catch { /* egal */ }
|
|
location.assign('/workspace/');
|
|
});
|
|
|
|
(async () => {
|
|
try {
|
|
const antwort = await hole('/workspace/api/ich');
|
|
if (!antwort.ok) { location.assign('/workspace/'); return; }
|
|
ich = await antwort.json();
|
|
} catch { return; }
|
|
|
|
$('wer').textContent = `${ich.name} · ${ich.rolle}`;
|
|
|
|
if (ich.rolle === 'admin') {
|
|
$('feld-creator').hidden = false;
|
|
$('feld-verant').hidden = false;
|
|
$('b-feld-creator').hidden = false;
|
|
$('b-feld-verant').hidden = false;
|
|
try {
|
|
const { personen } = await (await hole('/workspace/api/personen')).json();
|
|
for (const p of personen) {
|
|
for (const ziel of ['f-creator', 'f-verant', 'b-creator', 'b-verant']) {
|
|
const o = el('option', null, `${p.name} (${p.rolle})`);
|
|
o.value = String(p.id);
|
|
$(ziel).append(o);
|
|
}
|
|
}
|
|
} catch { /* Zuordnung bleibt dann leer -- kein Grund abzubrechen */ }
|
|
}
|
|
|
|
await laden();
|
|
})();
|
|
})();
|