/* =================================================================== Kalender: eigene Termine und Fristen aus den Aufgaben in einer Liste. Fristen sind nur eingeblendet -- geändert werden sie im Aufgabenbrett. =================================================================== */ (() => { 'use strict'; const $ = (id) => document.getElementById(id); const WOCHENTAGE = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag']; const ARTNAME = { call: 'Call', termin: 'Termin', review: 'Review', frist: 'Frist' }; let ich = null; let tage = 30; 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 || ''; }; const heute = new Date().toISOString().slice(0, 10); 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; } function datumHuebsch(iso) { const [j, m, t] = iso.slice(0, 10).split('-'); return `${t}.${m}.${j}`; } function wochentag(iso) { return WOCHENTAGE[new Date(iso.slice(0, 10) + 'T12:00:00').getDay()]; } /* ---------- Zeichnen --------------------------------------------------- */ function eintragTermin(t) { const k = el('div', 'eintrag'); k.dataset.art = t.art; k.dataset.erledigt = t.erledigt ? 'ja' : 'nein'; k.append(el('span', 'eintrag__zeit', t.beginn.slice(11, 16) + ' Uhr')); k.append(el('span', 'eintrag__titel', t.titel)); const knoepfe = el('div', 'eintrag__knoepfe'); knoepfe.append(el('span', 'eintrag__art', ARTNAME[t.art] || t.art)); knoepfe.append(tuKnopf(t.erledigt ? 'wieder offen' : 'erledigt', async () => { const a = await hole('/workspace/api/termine/' + t.id, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ erledigt: !t.erledigt }), }); if (!a.ok) { melde('Ändern ging nicht.'); return; } await laden(); })); if (ich.rolle === 'admin' || t.erstellt_von === ich.id) { knoepfe.append(tuKnopf('löschen', async () => { if (!confirm('Termin "' + t.titel + '" löschen?')) return; const a = await hole('/workspace/api/termine/' + t.id, { method: 'DELETE' }); if (!a.ok) { melde('Löschen ging nicht.'); return; } await laden(); })); } k.append(knoepfe); const zusatz = el('div', 'eintrag__zusatz'); if (t.dauer_min) zusatz.append(el('span', null, t.dauer_min + ' Min')); if (t.teilnehmer_name) zusatz.append(el('span', null, '· mit ' + t.teilnehmer_name)); if (ich.rolle === 'admin' && t.creator_name && t.creator_name !== t.teilnehmer_name) { zusatz.append(el('span', null, '· Bereich ' + t.creator_name)); } if (t.ort) { /* Links sollen klickbar sein, aber nur echte http(s)-Adressen -- sonst liesse sich hier javascript: einschleusen. */ if (/^https?:\/\//i.test(t.ort)) { const a = el('a', null, t.ort.replace(/^https?:\/\//i, '').slice(0, 60)); a.href = t.ort; a.target = '_blank'; a.rel = 'noopener noreferrer'; zusatz.append(a); } else { zusatz.append(el('span', null, '· ' + t.ort)); } } if (t.beschreibung) zusatz.append(el('span', null, '· ' + t.beschreibung)); if (zusatz.childElementCount) k.append(zusatz); return k; } function eintragFrist(f) { const k = el('div', 'eintrag'); k.dataset.art = 'frist'; k.append(el('span', 'eintrag__zeit', '—')); k.append(el('span', 'eintrag__titel', f.titel)); const knoepfe = el('div', 'eintrag__knoepfe'); knoepfe.append(el('span', 'eintrag__art', 'Frist')); const link = el('a', 'schritt', 'zur Aufgabe'); link.href = 'aufgaben.html'; knoepfe.append(link); k.append(knoepfe); const zusatz = el('div', 'eintrag__zusatz'); zusatz.append(el('span', null, 'Priorität ' + f.prioritaet)); if (f.verantwortlich_name) zusatz.append(el('span', null, '· ' + f.verantwortlich_name)); k.append(zusatz); return k; } function tuKnopf(text, tu) { const b = el('button', 'schritt', text); b.type = 'button'; b.addEventListener('click', async () => { b.disabled = true; melde(''); try { await tu(); } catch { /* umgeleitet */ } finally { b.disabled = false; } }); return b; } function zeichne(daten) { const ziel = $('liste'); ziel.textContent = ''; ziel.setAttribute('aria-busy', 'false'); /* Termine und Fristen nach Tag zusammenlegen. */ const nachTag = new Map(); for (const t of daten.termine) { const tag = t.beginn.slice(0, 10); if (!nachTag.has(tag)) nachTag.set(tag, { termine: [], fristen: [] }); nachTag.get(tag).termine.push(t); } for (const f of daten.fristen) { if (!nachTag.has(f.frist)) nachTag.set(f.frist, { termine: [], fristen: [] }); nachTag.get(f.frist).fristen.push(f); } if (!nachTag.size) { ziel.append(el('p', 'leer-hinweis', 'Für diesen Zeitraum ist nichts eingetragen.')); return; } for (const tag of [...nachTag.keys()].sort()) { const block = el('section', 'tag'); if (tag < heute) block.dataset.vergangen = 'ja'; const kopf = el('div', 'tag__kopf'); kopf.append(el('span', 'tag__datum', datumHuebsch(tag))); kopf.append(el('span', 'tag__wochentag', wochentag(tag))); if (tag === heute) kopf.append(el('span', 'tag__marke', 'Heute')); block.append(kopf); const { termine, fristen } = nachTag.get(tag); for (const t of termine) block.append(eintragTermin(t)); for (const f of fristen) block.append(eintragFrist(f)); ziel.append(block); } } async function laden() { melde(''); try { const a = await hole('/workspace/api/termine?tage=' + tage); if (!a.ok) { melde('Kalender konnte nicht geladen werden.'); return; } zeichne(await a.json()); } catch { /* umgeleitet */ } } /* ---------- Zeitraum ---------------------------------------------------- */ for (const b of document.querySelectorAll('.zeitraum .schritt')) { b.setAttribute('aria-pressed', String(Number(b.dataset.tage) === tage)); b.addEventListener('click', async () => { tage = Number(b.dataset.tage); for (const x of document.querySelectorAll('.zeitraum .schritt')) { x.setAttribute('aria-pressed', String(x === b)); } await laden(); }); } /* ---------- Neuer Termin ------------------------------------------------ */ const umschalten = (auf) => { $('neu').hidden = !auf; $('neu-oeffnen').hidden = auf; if (auf) $('f-titel').focus(); }; $('neu-oeffnen').addEventListener('click', () => { /* Vorbelegung: morgen 18:00 -- ein realistischer Zeitpunkt für einen Call, spart in der Regel zwei Klicks. */ const m = new Date(Date.now() + 86400_000); m.setHours(18, 0, 0, 0); $('f-beginn').value = new Date(m.getTime() - m.getTimezoneOffset() * 60000) .toISOString().slice(0, 16); umschalten(true); }); $('neu-abbrechen').addEventListener('click', () => { $('neu').reset(); $('neu-fehler').textContent = ''; umschalten(false); }); $('neu').addEventListener('submit', async (e) => { e.preventDefault(); $('neu-fehler').textContent = ''; const knopf = $('neu-speichern'); const daten = { titel: $('f-titel').value, art: $('f-art').value, beginn: $('f-beginn').value, dauer_min: Number($('f-dauer').value) || 30, ort: $('f-ort').value, beschreibung: $('f-text').value, }; if (ich.rolle === 'admin') { const wahl = $('f-teilnehmer').value; daten.teilnehmer_id = wahl || null; daten.creator_id = wahl || null; } knopf.disabled = true; try { const a = await hole('/workspace/api/termine', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(daten), }); if (!a.ok) { $('neu-fehler').textContent = (await a.json().catch(() => ({}))).fehler || 'Ging nicht.'; return; } $('neu').reset(); umschalten(false); await laden(); } catch { /* umgeleitet */ } finally { knopf.disabled = false; } }); /* ---------- 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; if (ich.rolle === 'admin') { $('feld-teilnehmer').hidden = false; try { const { personen } = await (await hole('/workspace/api/personen')).json(); for (const p of personen) { const o = document.createElement('option'); o.value = String(p.id); o.textContent = p.name + ' (' + p.rolle + ')'; $('f-teilnehmer').append(o); } } catch { /* Zuordnung bleibt leer */ } } await laden(); })(); })();