Erster echter Arbeitsbereich aus dem Konzept. Kanban mit offen / in Arbeit / Review / erledigt, dazu Prioritaet, Frist, Verantwortlicher und Zuordnung zu einem Creator-Bereich. Kern ist die Datentrennung, und die sitzt AUSSCHLIESSLICH im Server -- in jeder einzelnen Abfrage, an einer Stelle definiert (sichtbar()): admin sieht alles creator sieht seinen Bereich und was ihm zugewiesen ist scout sieht nur, was ihm zugewiesen ist Geprueft mit vier Testkonten: - Chef sieht 4, Luna 2, Mika 1, Sam 1 Aufgaben - Luna auf Mikas Aufgabe: 404 (nicht 403 -- sonst liesse sich durch Ausprobieren herausfinden, welche Nummern es gibt) - Luna legt Aufgabe mit creator_id=Mika an: wird still auf ihren eigenen Bereich umgebogen, Mika sieht sie nicht - Anfrage mit fremdem Origin: 403 - ohne Anmeldung: 401, ungueltiger Status/leerer Titel: 400 - Scout bekommt in der Personenliste nur sich selbst - Dashboard-Zahlen je Rolle korrekt eingegrenzt Weitere Punkte: - Texte werden im Browser nur ueber textContent gesetzt, nie innerHTML -- ein Aufgabentitel darf keine Auszeichnung einschleusen - ueberfaellig = Frist vorbei UND nicht erledigt; die Kachel faerbt sich nur, wenn wirklich etwas ansteht - erledigt_am wird gesetzt bzw. wieder geleert, wenn eine Aufgabe zurueckgeholt wird - Erledigtes verschwindet nicht, wie im Konzept gefordert
216 lines
7.3 KiB
JavaScript
216 lines
7.3 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;
|
|
k.append(el('h3', 'karte__titel', a.titel));
|
|
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; }
|
|
});
|
|
|
|
/* ---------- 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;
|
|
try {
|
|
const { personen } = await (await hole('/workspace/api/personen')).json();
|
|
for (const p of personen) {
|
|
for (const ziel of ['f-creator', 'f-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();
|
|
})();
|
|
})();
|