/* =================================================================== Ampel und Rückmeldungen an einem Eintrag. Zwei Dinge, bewusst getrennt gehalten: DIE AMPEL ist eine Beurteilung. Sie gehört dem Betreuer. Ein Creator sieht sie, kann sie aber nicht setzen -- sonst stünde alles auf grün. DIE NACHRICHT ist ein Gespräch. Sie gehört beiden. Ein Creator, der auf eine Bewertung nicht antworten kann, bekommt ein Urteil statt einer Betreuung. Drei Stufen, nicht fünf. "Reicht das schon?" hat drei ehrliche Antworten: ja, noch nicht, noch nicht angesehen. Eine Zahl von 1 bis 5 klingt genauer und ist es nicht -- am Ende steht überall die 3. =================================================================== */ (() => { 'use strict'; const el = (tag, klasse, text) => { const k = document.createElement(tag); if (klasse) k.className = klasse; if (text !== undefined) k.textContent = text; return k; }; 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; } const wann = (iso) => (iso ? new Date(iso).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }) : ''); const STUFEN = [ ['gut', 'Passt so'], ['verbessern', 'Noch verbessern'], ]; /** Hängt Ampel und Rückmeldungen an eine Eintragskarte. * karte: das Element, an das angehängt wird * id: die Nummer des Eintrags * vorschau: { ampel, anzahl } aus der Sammelabfrage -- damit die * Liste nicht für jeden Eintrag einzeln nachfragt */ window.ampelAnhaengen = function (karte, id, vorschau = {}) { const zeile = el('div', 'ampel'); const stufe = vorschau.ampel?.stufe || 'offen'; zeile.dataset.stufe = stufe; /* Der Zustand, immer sichtbar -- auch für den Creator. Das ist der ganze Sinn: Er soll sehen, wo er steht, ohne fragen zu müssen. */ const marke = el('span', 'ampel__marke', stufe === 'gut' ? 'Passt so' : stufe === 'verbessern' ? 'Noch verbessern' : 'Noch nicht angesehen'); zeile.append(marke); if (vorschau.ampel?.von_name) { zeile.append(el('span', 'ampel__von', `${vorschau.ampel.von_name} · ${wann(vorschau.ampel.am)}`)); } /* Die Begründung steht DANEBEN, nicht versteckt. "Noch verbessern" ohne Grund ist nicht umsetzbar, nur entmutigend. */ if (vorschau.ampel?.begruendung) { zeile.append(el('p', 'ampel__grund', vorschau.ampel.begruendung)); } const auf = el('button', 'schritt', vorschau.anzahl ? `Rückmeldung (${vorschau.anzahl})` : 'Rückmeldung'); auf.type = 'button'; auf.addEventListener('click', () => oeffnen(karte, id, auf)); zeile.append(auf); karte.append(zeile); }; /* ---------- Der ausklappbare Bereich ---------------------------------- */ async function oeffnen(karte, id, knopf) { const vorhanden = karte.querySelector('.rueckmeldung'); if (vorhanden) { vorhanden.remove(); knopf.dataset.offen = ''; return; } knopf.dataset.offen = 'ja'; let daten; try { const a = await hole(`/workspace/api/eintrag/${id}/rueckmeldung`); if (!a.ok) return; daten = await a.json(); } catch { return; } const kasten = el('div', 'rueckmeldung'); /* ---- Bewerten: nur die Betreuung ---- */ if (daten.darf_bewerten) { const b = el('div', 'rueckmeldung__bewerten'); b.append(el('p', 'feldschild', 'Bewertung')); const knoepfe = el('div', 'ampel__wahl'); for (const [wert, name] of STUFEN) { const k = el('button', 'ampel__knopf', name); k.type = 'button'; k.dataset.stufe = wert; if (daten.ampel?.stufe === wert) k.dataset.an = 'ja'; k.addEventListener('click', async () => { let grund = daten.ampel?.begruendung || ''; /* "Noch verbessern" verlangt einen Grund -- der Server lehnt es sonst ab, und das ist richtig so. Hier wird deshalb gleich danach gefragt statt eine Fehlermeldung zu zeigen. */ if (wert === 'verbessern') { const ag = await frageNach({ titel: 'Noch verbessern', was: 'Der Creator sieht diesen Satz an seinem Eintrag.', bleibt: 'Der Eintrag bleibt stehen – er wird nur als „noch ' + 'verbessern" markiert und kann überarbeitet werden.', ja: 'Zurückgeben', grund: 'Was genau soll verbessert werden?', grundPflicht: true, platzhalter: 'z. B. Das Licht ist zu dunkel – bitte heller nachdrehen.', }); if (!ag) return; grund = ag.grund; } k.disabled = true; try { const a = await hole(`/workspace/api/eintrag/${id}/ampel`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ stufe: wert, begruendung: grund }), }); if (!a.ok) { alert(sagWas((await a.json().catch(() => ({}))).fehler, 'Ging nicht.')); k.disabled = false; return; } /* Die Karte neu aufbauen lassen -- die Seite weiss selbst am besten, wie sie aussieht. */ document.dispatchEvent(new CustomEvent('ampel-geaendert', { detail: { id } })); } catch { k.disabled = false; } }); knoepfe.append(k); } b.append(knoepfe); kasten.append(b); } else { kasten.append(el('p', 'rueckmeldung__hinweis', 'Die Bewertung setzt deine Betreuung. Hier kannst du antworten und Fragen stellen.')); } /* ---- Das Gespräch ---- */ const verlauf = el('div', 'nachrichten'); zeichneNachrichten(verlauf, daten.nachrichten, daten.ich, id); kasten.append(verlauf); /* ---- Schreiben: JEDER Beteiligte ---- */ const form = el('form', 'nachricht-neu'); const feld = el('textarea'); feld.rows = 2; feld.maxLength = 2000; feld.placeholder = daten.darf_bewerten ? 'Rückmeldung an den Creator …' : 'Frage oder Antwort an deine Betreuung …'; form.append(feld); const senden = el('button', 'knopf knopf--klein', 'Senden'); senden.type = 'submit'; form.append(senden); form.addEventListener('submit', async (e) => { e.preventDefault(); if (!feld.value.trim()) return; senden.disabled = true; try { const a = await hole(`/workspace/api/eintrag/${id}/nachricht`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: feld.value }), }); if (!a.ok) { alert(sagWas((await a.json().catch(() => ({}))).fehler, 'Ging nicht.')); return; } const { nachrichten } = await a.json(); feld.value = ''; zeichneNachrichten(verlauf, nachrichten, daten.ich, id); } catch { /* umgeleitet */ } finally { senden.disabled = false; } }); kasten.append(form); karte.append(kasten); } function zeichneNachrichten(ziel, liste, ichId, eintragId) { ziel.textContent = ''; if (!liste.length) { ziel.append(el('p', 'nachrichten__leer', 'Noch keine Rückmeldung.')); return; } for (const n of liste) { const z = el('div', 'nachricht'); if (n.von === ichId) z.dataset.eigen = 'ja'; z.dataset.rolle = n.von_rolle || ''; const kopf = el('div', 'nachricht__kopf'); kopf.append(el('span', 'nachricht__von', n.von_name || 'Unbekannt')); kopf.append(el('span', 'nachricht__wann', wann(n.am))); /* Nur die eigene lässt sich zurücknehmen. Fremde zu löschen hieße, ein Gespräch nachträglich umzuschreiben. */ if (n.von === ichId) { const weg = el('button', 'nachricht__weg', '×'); weg.type = 'button'; weg.title = 'Eigene Nachricht zurücknehmen'; weg.addEventListener('click', async () => { if (!await frageNach({ titel: 'Nachricht zurücknehmen?', was: 'Der Text verschwindet für alle, die ihn noch nicht gelesen haben.', bleibt: 'Wer ihn schon gelesen hat, erinnert sich trotzdem daran.', endgueltig: true, ja: 'Zurücknehmen', })) return; try { const a = await hole(`/workspace/api/eintrag/${eintragId}/nachricht/${n.id}`, { method: 'DELETE' }); if (a.ok) z.remove(); } catch { /* umgeleitet */ } }); kopf.append(weg); } z.append(kopf); z.append(el('p', 'nachricht__text', n.text)); ziel.append(z); } } /** Holt Ampeln und Nachrichtenzahlen für eine ganze Liste auf einmal. * Zwanzig Einträge einzeln abzufragen wären zwanzig Anfragen. */ window.ampelnHolen = async function (ids) { if (!ids.length) return { ampeln: {}, anzahlNachrichten: {} }; try { const a = await hole('/workspace/api/eintrag/ampeln?ids=' + ids.join(',')); if (!a.ok) return { ampeln: {}, anzahlNachrichten: {} }; return await a.json(); } catch { return { ampeln: {}, anzahlNachrichten: {} }; } }; })();