Wunsch: "ich will auch immer bei jeder seite auch immer einen knopf
haben damit ich zurueck gehen kann auf die seite vorher."
Neues gemeinsames Modul assets/js/kopf.js. Blindes history.back() reicht
dafuer nicht: Wer die Adresse direkt eingibt oder gerade von der
Anmeldung kommt, landet damit ausserhalb des Workspace oder wieder im
Anmeldeformular. Deshalb wird zuerst geprueft, woher der Aufruf kam:
* vorherige Seite im Workspace -> "Zurueck", history.back()
(fuehrt wirklich dorthin zurueck, samt Bildlaufposition)
* kein Verlauf, aber Unterseite -> "Uebersicht", geht zur Startseite
* kein Verlauf auf der Startseite -> Knopf bleibt verborgen
Ein Knopf, der nichts tut, ist schlimmer als keiner.
Der Knopf sitzt links in der Kopfleiste, an derselben Stelle wie im
Browser.
Nebenbei aufgeraeumt: Das Abmelden stand bisher in jeder der fuenf
Seitendateien noch einmal -- fuenfmal derselbe Block, fuenfmal eine
Stelle zum Vergessen. Liegt jetzt ebenfalls in kopf.js.
Geprueft: Start nach Login -> verborgen; Aufgaben von Start ->
"Zurueck", fuehrt zurueck; Kalender direkt aufgerufen -> "Uebersicht".
Keine Konsolenfehler, Abmelden funktioniert weiterhin.
Nur statische Dateien, kein Neustart noetig.
285 lines
9.6 KiB
JavaScript
285 lines
9.6 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 ----------------------------------------------------- */
|
|
|
|
|
|
(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();
|
|
})();
|
|
})();
|