/* =================================================================== 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') { grund = prompt('Was genau soll verbessert werden?\n\n' + 'Ohne diesen Satz kann der Creator nichts damit anfangen.', grund) || ''; if (grund.trim().length < 5) return; } 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 (!confirm('Diese Nachricht 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: {} }; } }; })();