Deutsch oeffnet mit „ und schliesst mit “. An 199 Stellen, die ein
Mensch liest, stand als Schlusszeichen ein GERADES " -- „Nächstes"
statt „Nächstes“. Auf sechzehn oeffentlichen Seiten, in vierzig
Dateien des Workspace und in zwoelf Servermodulen, die Texte
verschicken.
Auf dem Bildschirm sieht man den Unterschied sofort. Beim Schreiben
nicht: Das gerade " liegt auf der Tastatur, die anderen nicht.
WARUM EIN ERSTER ANLAUF ZURUECKGENOMMEN WURDE
Ein gerades " ist an vielen Stellen SYNTAX und kein Schriftzeichen --
Grenze einer Zeichenkette, Grenze eines HTML-Attributs, Zeichen in
einem regulaeren Ausdruck. Wer stumpf ersetzt, macht aus
„<a href="https://… ein kaputtes Attribut
/^["'„»\s]+|["'“«.\s]+$/ einen kaputten Ausdruck
"… nichts „mal " + "eben …" eine kaputte Zeichenkette
DIE UNTERSCHEIDUNG LAEUFT AN MERKMALEN, NICHT AN EINER LISTE
< > = dazwischen -> HTML-Marke oder Attribut
endet auf Leerzeichen -> die Zeichenkette hoert hier auf, der
Satz geht in der naechsten Zeile weiter.
Ein deutsches Schlusszeichen steht NIE
hinter einem Leerzeichen.
Rueckstrich mittendrin -> regulaerer Ausdruck
${ ohne } -> mitten in einem Ausdruck
Eine Liste erlaubter Ausnahmen waere die naechste, die niemand
pflegt. Zwoelf Stellen bleiben dadurch stehen, alle zwoelf einzeln
angesehen und alle zwoelf zu Recht -- dort steht das richtige
Schlusszeichen ohnehin weiter unten im Satz.
Fuenf davon waren allerdings ECHTE Fehler HINTER dem Link
(`…>HasiDog</a>".`) -- die erste Regel hatte nur das Attribut
gesehen, nicht den Satz danach. Gezielt nachgezogen.
Ein maskiertes `\"` in workspace-vorlagen.js (28 Hooks) wird zu “ --
ohne Rueckstrich, denn “ begrenzt nichts.
KOMMENTARE BLEIBEN, WIE SIE SIND. Dort liest es niemand ausser mir;
eine Wache, die auch Kosmetik anmahnt, wird weggeklickt. Beim ersten
Messen fielen ausserdem acht Stellen aus buehne.html faelschlich an,
weil `/* */` in HTML (in <style> und <script>) nicht ausgeblendet
war -- jetzt schon.
DIE WACHE DAZU
pruef-struktur prueft es ab sofort mit derselben Regel: 322 Dateien
mit sichtbarem Text, 0 Funde, und die zwoelf bewussten Ausnahmen
werden GEZAEHLT und genannt (erlaubt: 12). Eine Ausnahme, die niemand
sieht, waechst -- und irgendwann steht der echte Fall darin.
GEPRUEFT
pruef-struktur 59 -> 68 Pruefungen, 0 Fehler
node --check auf allen geaenderten JS-Dateien
nachgemessen: 199 geaendert, 12 mit Grund stehen geblieben
gruen geblieben: bewerbung-aufgaben 163, nachwuchs 262,
reaktion 421, support 63, content 45, terminregel 35, treff 85
Und nachgesehen, ob eine Pruefung noch die alte Schreibweise
ERWARTET: 13 Fundstellen, alle dreizehn nur Text in ihrer eigenen
Ausgabe, keine einzige ein Vergleich mit dem Seitentext.
GEGENPROBE: In reaktion.html ein Schlusszeichen zurueckgedreht ->
„workspace/reaktion.html:546 „Nächstes"", mit Datei und Zeile. Und
die Erkennung einzeln gegen HTML-Attribut, fortgesetzte
Zeichenkette, regulaeren Ausdruck und eingesetzten Wert geprueft.
Stempel gesetzt: workspace 670 Verweise in 45 Dateien, oeffentlich
554 in 48 Seiten.
Co-Authored-By: Claude Opus 5 <[email protected]>
611 lines
24 KiB
JavaScript
611 lines
24 KiB
JavaScript
/* =====================================================================
|
||
SUPPORT (24.09.2026)
|
||
|
||
Filipe: „jeder kann da probleme von der seite melden und hinweisen.
|
||
fotos mit schicken mit einem text. … so einfach wie möglich, will
|
||
nichts kompliziertes oder zu viel. kurz und knapp."
|
||
|
||
EINE SEITE, ZWEI ANSICHTEN. Wer meldet, sieht ein Feld und seine
|
||
eigenen Meldungen. DogFather und die rechte Hand sehen den Eingang:
|
||
alles, nach Stand sortiert, mit Bild und zwei Knöpfen.
|
||
|
||
WELCHE ANSICHT GILT, ENTSCHEIDET DER SERVER. Er schickt `leitung`
|
||
in der Antwort — diese Datei fragt nicht nach der Rolle. Eine
|
||
Rollenprüfung im Browser wäre eine zweite Stelle mit derselben
|
||
Regel, und es wäre die, an der man sie umgehen kann.
|
||
|
||
KEIN innerHTML MIT FREMDEM TEXT. Alles, was jemand geschrieben hat,
|
||
geht durch `textContent`. Hier landet Text aus einem Fehlerbericht —
|
||
und genau dort steht irgendwann ein `<script>`, weil jemand es
|
||
ausprobiert.
|
||
===================================================================== */
|
||
(() => {
|
||
'use strict';
|
||
|
||
const $ = (id) => document.getElementById(id);
|
||
const el = (was, klasse, text) => {
|
||
const e = document.createElement(was);
|
||
if (klasse) e.className = klasse;
|
||
if (text !== undefined) e.textContent = text;
|
||
return e;
|
||
};
|
||
|
||
let leitung = false;
|
||
let meldungen = [];
|
||
let filter = 'offen';
|
||
let gewaehltesBild = null;
|
||
|
||
/* Anzeige-Wörter, keine Regel. Der Server schickt den Schlüssel; was
|
||
daraus auf dem Bildschirm wird, ist eine Frage der Sprache. */
|
||
const STAND_WORT = {
|
||
neu: 'neu',
|
||
in_arbeit: 'wird bearbeitet',
|
||
/* ==== DER VIERTE STAND (25.09.2026) ==========================
|
||
|
||
Filipe: „damit die bescheid wissen und dan anklicken koennen,
|
||
es funktioniert, oder noch nicht."
|
||
|
||
Das Wort sagt, WER DRAN IST -- nicht, was passiert ist. „Wartet
|
||
auf Antwort" waere aus Sicht der Leitung richtig und aus Sicht
|
||
des Melders falsch: Er wartet nicht, er ist gefragt. */
|
||
wartet: 'geht es wieder?',
|
||
erledigt: 'erledigt',
|
||
};
|
||
|
||
function melde(id, text) {
|
||
const f = $(id);
|
||
if (!f) return;
|
||
f.textContent = text || '';
|
||
f.hidden = !text;
|
||
}
|
||
|
||
async function hole(pfad, einstellungen) {
|
||
const a = await fetch(pfad, { credentials: 'same-origin', ...einstellungen });
|
||
if (a.status === 401) { location.href = 'index.html'; throw new Error('weg'); }
|
||
return a;
|
||
}
|
||
|
||
/** Was der Server als Fehler geschickt hat — oder ein Ersatz.
|
||
*
|
||
* Die Antwort wegzuwerfen und „Ging nicht" zu schreiben schickt
|
||
* einen auf die Suche nach etwas, das der Server längst gesagt hat. */
|
||
async function fehlerText(a, ersatz) {
|
||
try {
|
||
const j = await a.json();
|
||
return typeof j?.fehler === 'string' && j.fehler ? j.fehler : ersatz;
|
||
} catch { return ersatz; }
|
||
}
|
||
|
||
/** „vor 3 Tagen" statt eines Zeitstempels.
|
||
*
|
||
* Bei einer Meldung ist die Frage „wie lange liegt das schon?",
|
||
* nicht „wann genau war das?". Das genaue Datum bleibt als Titel. */
|
||
function herZeit(iso) {
|
||
if (!iso) return '';
|
||
const ms = Date.now() - Date.parse(iso);
|
||
if (!Number.isFinite(ms)) return '';
|
||
const min = Math.floor(ms / 60000);
|
||
if (min < 2) return 'gerade eben';
|
||
if (min < 60) return `vor ${min} Min.`;
|
||
const std = Math.floor(min / 60);
|
||
if (std < 24) return `vor ${std} Std.`;
|
||
const tage = Math.floor(std / 24);
|
||
if (tage === 1) return 'gestern';
|
||
if (tage < 31) return `vor ${tage} Tagen`;
|
||
const monate = Math.floor(tage / 30.44);
|
||
return monate < 12 ? `vor ${monate} Mon.` : 'vor über einem Jahr';
|
||
}
|
||
|
||
function genau(iso) {
|
||
if (!iso) return '';
|
||
const d = new Date(iso);
|
||
if (Number.isNaN(d.getTime())) return '';
|
||
const z = (n) => String(n).padStart(2, '0');
|
||
return `${z(d.getDate())}.${z(d.getMonth() + 1)}.${d.getFullYear()} `
|
||
+ `${z(d.getHours())}:${z(d.getMinutes())}`;
|
||
}
|
||
|
||
/* ===================================================================
|
||
MELDEN
|
||
=================================================================== */
|
||
|
||
function bildZeigen(datei) {
|
||
gewaehltesBild = datei || null;
|
||
const name = $('bild-name');
|
||
const weg = $('bild-weg');
|
||
const vorschau = $('vorschau');
|
||
if (!datei) {
|
||
name.textContent = '';
|
||
weg.hidden = true;
|
||
vorschau.hidden = true;
|
||
$('vorschau-bild').removeAttribute('src');
|
||
return;
|
||
}
|
||
name.textContent = `${datei.name} · ${Math.round(datei.size / 1024)} KB`;
|
||
weg.hidden = false;
|
||
/* DIE VORSCHAU KOMMT AUS DER DATEI SELBST, nicht vom Server: Sie
|
||
ist noch gar nicht hochgeladen. `URL.createObjectURL` zeigt das
|
||
Bild aus dem Arbeitsspeicher — wer sieht, was er anhängt, hängt
|
||
nicht versehentlich das falsche an. */
|
||
const url = URL.createObjectURL(datei);
|
||
const img = $('vorschau-bild');
|
||
img.onload = () => URL.revokeObjectURL(url);
|
||
img.src = url;
|
||
vorschau.hidden = false;
|
||
}
|
||
|
||
function meldenEinrichten() {
|
||
$('bild-waehlen').addEventListener('click', () => $('bild').click());
|
||
$('bild-weg').addEventListener('click', () => {
|
||
$('bild').value = '';
|
||
bildZeigen(null);
|
||
});
|
||
$('bild').addEventListener('change', (e) => {
|
||
const datei = e.target.files?.[0] || null;
|
||
if (!datei) { bildZeigen(null); return; }
|
||
/* DIE GRENZE STEHT HIER NUR ALS FREUNDLICHKEIT. Abgelehnt wird
|
||
am Server; dieser Hinweis erspart es, zwölf Megabyte erst
|
||
hochzuladen, um danach eine Absage zu lesen. */
|
||
if (datei.size > 12 * 1024 * 1024) {
|
||
melde('melden-fehler', 'Das Bild ist größer als 12 MB – bitte ein kleineres.');
|
||
$('bild').value = '';
|
||
bildZeigen(null);
|
||
return;
|
||
}
|
||
melde('melden-fehler', '');
|
||
bildZeigen(datei);
|
||
});
|
||
|
||
$('melden').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const text = $('text').value.trim();
|
||
if (text.length < 5) {
|
||
melde('melden-fehler', 'Schreib bitte kurz, was los ist – ein Satz reicht.');
|
||
$('text').focus();
|
||
return;
|
||
}
|
||
const knopf = $('senden');
|
||
knopf.disabled = true;
|
||
melde('melden-fehler', '');
|
||
$('melden-hinweis').textContent = 'wird geschickt …';
|
||
try {
|
||
/* DER TEXT REIST IM KOPF, das Bild im Rumpf — derselbe Weg wie
|
||
bei Chat-Anhängen. Ein Weg für beides, damit es keinen
|
||
Zustand „Meldung da, Bild fehlt noch" gibt. */
|
||
const kopf = { 'x-text': encodeURIComponent(text), 'x-seite': location.pathname };
|
||
const a = await hole('/workspace/api/support', {
|
||
method: 'POST',
|
||
headers: kopf,
|
||
body: gewaehltesBild || new Blob([]),
|
||
});
|
||
if (!a.ok) {
|
||
melde('melden-fehler', await fehlerText(a, 'Das hat nicht geklappt.'));
|
||
return;
|
||
}
|
||
$('text').value = '';
|
||
$('bild').value = '';
|
||
bildZeigen(null);
|
||
$('melden-hinweis').textContent = 'Danke – ist angekommen.';
|
||
setTimeout(() => { $('melden-hinweis').textContent = ''; }, 6000);
|
||
await laden();
|
||
} catch { /* umgeleitet */ } finally {
|
||
knopf.disabled = false;
|
||
if ($('melden-hinweis').textContent === 'wird geschickt …') {
|
||
$('melden-hinweis').textContent = '';
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
/* ===================================================================
|
||
EINE MELDUNG ALS KARTE
|
||
=================================================================== */
|
||
|
||
function bildAnhaengen(karte, m) {
|
||
if (!m.bild) return;
|
||
const kasten = el('div', 's-karte__bild');
|
||
const a = el('a');
|
||
a.href = m.bild;
|
||
a.target = '_blank';
|
||
a.rel = 'noopener';
|
||
a.title = 'In voller Größe ansehen';
|
||
const img = document.createElement('img');
|
||
img.src = m.bild;
|
||
img.alt = 'Bild zur Meldung';
|
||
img.loading = 'lazy';
|
||
/* BREITE UND HÖHE STEHEN DRAN, wenn der Server sie kennt: Dann
|
||
reserviert der Browser den Platz, bevor das Bild da ist, und die
|
||
Liste springt beim Laden nicht. */
|
||
if (m.bild_breite > 0 && m.bild_hoehe > 0) {
|
||
img.width = m.bild_breite;
|
||
img.height = m.bild_hoehe;
|
||
}
|
||
a.append(img);
|
||
kasten.append(a);
|
||
karte.append(kasten);
|
||
}
|
||
|
||
function karteBauen(m) {
|
||
const k = el('article', 's-karte');
|
||
k.dataset.stand = m.stand;
|
||
k.id = `meldung-${m.id}`;
|
||
|
||
const kopf = el('div', 's-karte__kopf');
|
||
const marke = el('span', 's-marke', STAND_WORT[m.stand] || m.stand);
|
||
marke.dataset.stand = m.stand;
|
||
kopf.append(marke);
|
||
if (leitung && m.von) {
|
||
kopf.append(el('span', 's-karte__von', m.von));
|
||
}
|
||
const zeit = el('span', 's-karte__zeit', herZeit(m.erstellt));
|
||
zeit.title = genau(m.erstellt);
|
||
kopf.append(zeit);
|
||
k.append(kopf);
|
||
|
||
/* `textContent` mit erhaltenen Zeilenumbrüchen (das CSS setzt
|
||
`white-space: pre-wrap`). Kein innerHTML, siehe der Kopf. */
|
||
k.append(el('p', 's-karte__text', m.text));
|
||
bildAnhaengen(k, m);
|
||
|
||
if (leitung && m.seite) {
|
||
const wo = el('p', 's-karte__wo', 'gemeldet auf ' + m.seite);
|
||
if (m.geraet) wo.title = m.geraet;
|
||
k.append(wo);
|
||
}
|
||
|
||
/* ==== DER VERLAUF STATT NUR DER LETZTEN ANTWORT (25.09.2026) ==
|
||
|
||
Filipe: „mit dem gedanken das manchmal sachen mehrmal nicht
|
||
sofort perfekt sein werden."
|
||
|
||
Hier stand nur `m.antwort` -- die LETZTE. Bei Runde drei war
|
||
damit nicht mehr zu sehen, was beim ersten Mal versucht wurde,
|
||
und genau das loest einen wiederkehrenden Fehler.
|
||
|
||
DIE RUNDEN STEHEN UNTEREINANDER, jede mit dem, was die Leitung
|
||
gesagt hat, und dem, was der Melder daraufhin geantwortet hat.
|
||
Gibt es noch keine Runde (die Meldung ist frisch), steht wie
|
||
bisher die blosse Antwort da -- Meldungen von vor diesem Umbau
|
||
haben keinen Verlauf, und ein leerer Kasten waere schlechter
|
||
als der alte Satz. */
|
||
const verlauf = Array.isArray(m.verlauf) ? m.verlauf : [];
|
||
if (verlauf.length) {
|
||
const liste = el('div', 's-verlauf');
|
||
for (const r of verlauf) {
|
||
const runde = el('div', 's-runde');
|
||
const kopfR = el('div', 's-runde__kopf');
|
||
kopfR.append(el('span', 's-runde__nr', 'Runde ' + r.nr));
|
||
if (r.von) kopfR.append(el('span', 's-runde__von', r.von));
|
||
runde.append(kopfR);
|
||
runde.append(el('p', 's-runde__antwort', r.antwort));
|
||
if (r.urteil) {
|
||
const u = el('p', 's-runde__urteil');
|
||
u.dataset.urteil = r.urteil;
|
||
u.append(el('span', 's-runde__urteil-wort',
|
||
r.urteil === 'geht' ? 'Geht wieder' : 'Ging noch nicht'));
|
||
if (r.urteil_text) u.append(document.createTextNode(' · ' + r.urteil_text));
|
||
runde.append(u);
|
||
}
|
||
liste.append(runde);
|
||
}
|
||
k.append(liste);
|
||
} else if (m.antwort) {
|
||
const ant = el('div', 's-antwort');
|
||
ant.append(el('span', 's-antwort__schild', 'Antwort'));
|
||
ant.append(el('p', 's-antwort__text', m.antwort));
|
||
k.append(ant);
|
||
}
|
||
|
||
/* ==== DER MELDER IST GEFRAGT (25.09.2026) ====================
|
||
|
||
Filipe: „und erst wenn es funktioniert gedrueckt wird, will ich
|
||
dass alles richtig fertig ist."
|
||
|
||
`darf_bestaetigen` KOMMT VOM SERVER -- er weiss, wessen Meldung
|
||
es ist, und diese Datei bekommt jeder, der die Seite oeffnet.
|
||
Ein Rollenvergleich im Browser waere die zweite Wahrheit, die in
|
||
diesem Haus diese Woche viermal veraltet ist. */
|
||
if (m.darf_bestaetigen) k.append(knoepfeFuerMelder(m));
|
||
else if (leitung && m.stand !== 'erledigt') k.append(knoepfeFuerLeitung(m));
|
||
return k;
|
||
}
|
||
|
||
/** Die zwei Knoepfe des Melders.
|
||
*
|
||
* „GEHT WIEDER" ZUERST und ohne Rueckfrage: Es ist der haeufige und
|
||
* der harmlose Fall -- wer bestaetigt, dass etwas geht, soll dafuer
|
||
* kein Fenster wegklicken muessen.
|
||
*
|
||
* „NOCH NICHT" FRAGT NACH. Ohne ein Wort dazu faengt die Suche von
|
||
* vorn an, und die naechste Runde waere dieselbe wie die letzte.
|
||
* Der Server verlangt es ohnehin; hier wird es gefragt, statt eine
|
||
* Absage zu kassieren.
|
||
*/
|
||
function knoepfeFuerMelder(m) {
|
||
const reihe = el('div', 's-karte__tun');
|
||
|
||
const geht = el('button', 'schritt s-tun--geht', 'Geht wieder ✓');
|
||
geht.type = 'button';
|
||
geht.addEventListener('click', () => rueckmelden(m, true, ''));
|
||
reihe.append(geht);
|
||
|
||
const nicht = el('button', 'schritt', 'Noch nicht …');
|
||
nicht.type = 'button';
|
||
nicht.addEventListener('click', async () => {
|
||
const antwort = await window.frageNach?.({
|
||
titel: 'Geht es noch nicht?',
|
||
was: 'Die Meldung geht zurück an DogFather und die rechte Hand.',
|
||
bleibt: 'Alles bisher Geschriebene bleibt stehen – ihr fangt nicht von vorn an.',
|
||
ja: 'Zurückgeben',
|
||
grund: 'Was geht noch nicht?',
|
||
grundPflicht: true,
|
||
grundMin: 3,
|
||
platzhalter: 'z. B. Der Knopf geht jetzt, aber das Datum stimmt immer noch nicht.',
|
||
});
|
||
if (!antwort) return;
|
||
await rueckmelden(m, false, antwort.grund || '');
|
||
});
|
||
reihe.append(nicht);
|
||
return reihe;
|
||
}
|
||
|
||
async function rueckmelden(m, geht, text) {
|
||
melde('eingang-fehler', '');
|
||
try {
|
||
const a = await hole(`/workspace/api/support/${m.id}/rueckmeldung`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ geht, text }),
|
||
});
|
||
/* DURCH DIESELBE UEBERSETZUNG WIE ALLES ANDERE (25.09.2026,
|
||
gefunden von pruef-meldungen). Hier stand `f.fehler` roh --
|
||
schickte der Server einmal eine Kennung statt eines Satzes
|
||
(`nicht_gefunden`), stuende das mit Unterstrich auf dem
|
||
Bildschirm. `fehlerText` gibt es in dieser Datei seit jeher;
|
||
die zwei anderen Stellen benutzen es, diese neue nicht.
|
||
Genau so entsteht eine Ausnahme: nicht aus Absicht, sondern
|
||
weil man die Hausregel beim Neuschreiben nicht danebenliegen
|
||
hatte. */
|
||
if (!a.ok) {
|
||
melde('eingang-fehler', await fehlerText(a, 'Das ging gerade nicht.'));
|
||
return;
|
||
}
|
||
await laden();
|
||
} catch { /* hole() hat schon gemeldet */ }
|
||
}
|
||
|
||
/** Die zwei Handgriffe der Leitung.
|
||
*
|
||
* ZWEI, NICHT FÜNF. „Ich kümmere mich" und „erledigt". Eine
|
||
* Zuweisung an eine bestimmte Person, Prioritäten, Kategorien — das
|
||
* wäre ein Ticketsystem für zwei Menschen, die nebeneinander
|
||
* sitzen. */
|
||
function knoepfeFuerLeitung(m) {
|
||
const reihe = el('div', 's-karte__tun');
|
||
|
||
if (m.stand === 'neu') {
|
||
const dran = el('button', 'schritt', 'Ich kümmere mich');
|
||
dran.type = 'button';
|
||
dran.addEventListener('click', () => aendern(m, 'in_arbeit', ''));
|
||
reihe.append(dran);
|
||
}
|
||
|
||
/* ==== WER IST DRAN? (25.09.2026) =============================
|
||
|
||
Liegt die Meldung beim Melder, ist die Leitung NICHT dran --
|
||
und ein Knopf „Behoben – nachfragen ..." waere dort schlicht
|
||
falsch: Sie hat ja schon nachgefragt. Gefunden hat das kein
|
||
Test, sondern ein Bildschirmfoto der fertigen Seite.
|
||
|
||
DER HINWEIS STEHT VOR DEM KNOPF, nicht statt seiner: Manchmal
|
||
faellt einem hinterher noch etwas ein, und dann soll man es
|
||
schicken koennen. Der Knopf heisst dann aber, was er tut. */
|
||
let hinweis = null;
|
||
if (m.stand === 'wartet') {
|
||
hinweis = el('p', 's-karte__wartet');
|
||
hinweis.append(el('span', 's-karte__wartet-punkt'));
|
||
hinweis.append(document.createTextNode(
|
||
m.von ? `Liegt bei ${m.von} – sie oder er sagt, ob es wieder geht.`
|
||
: 'Liegt beim Melder – er sagt, ob es wieder geht.'));
|
||
}
|
||
|
||
/* DER KNOPF HEISST ANDERS, WEIL ER ETWAS ANDERES TUT (25.09.2026).
|
||
|
||
„Erledigt" hiess: zu. Jetzt geht die Meldung damit zuerst an den
|
||
MELDER, und erst sein Druck schliesst sie. Ein Knopf, der
|
||
„Erledigt" sagt und in Wahrheit eine Frage stellt, waere genau
|
||
die Sorte Beschriftung, die man einmal glaubt und danach nie
|
||
wieder. */
|
||
const fertig = el('button', 'schritt',
|
||
m.stand === 'wartet' ? 'Noch etwas nachschicken …' : 'Behoben – nachfragen …');
|
||
fertig.type = 'button';
|
||
fertig.addEventListener('click', async () => {
|
||
/* WER ZUMACHT, SAGT WARUM. Der Melder sieht nur diesen Satz —
|
||
ohne ihn weiß er nicht, ob etwas behoben wurde oder ob
|
||
niemand Zeit hatte. Der Server verlangt ihn ohnehin; hier
|
||
wird er gefragt, statt eine Absage zu kassieren. */
|
||
const nachtrag = m.stand === 'wartet';
|
||
const antwort = await window.frageNach?.({
|
||
titel: nachtrag ? 'Noch etwas nachschicken?' : 'Behoben – beim Melder nachfragen?',
|
||
was: nachtrag
|
||
? 'Dein neuer Satz ERSETZT den alten in dieser Runde – der Melder sieht nur ihn.'
|
||
: 'Er bekommt deinen Satz und zwei Knöpfe: „Geht wieder“ oder „Noch nicht“.',
|
||
bleibt: nachtrag
|
||
? 'Die Meldung bleibt bei ihm liegen. Der Verlauf davor bleibt stehen.'
|
||
: 'Erledigt ist sie erst, wenn ER bestätigt. Sagt er „noch nicht“, '
|
||
+ 'kommt sie zurück – mit allem, was ihr bisher geschrieben habt.',
|
||
ja: nachtrag ? 'Nachschicken' : 'Nachfragen',
|
||
grund: 'Was ist daraus geworden?',
|
||
grundPflicht: true,
|
||
grundMin: 3,
|
||
platzhalter: nachtrag
|
||
? 'z. B. Behoben, der Knopf geht wieder – und das Datum stimmt jetzt auch.'
|
||
: 'z. B. Behoben, der Knopf geht wieder.',
|
||
});
|
||
if (!antwort) return;
|
||
await aendern(m, 'erledigt', antwort.grund || '');
|
||
});
|
||
reihe.append(fertig);
|
||
|
||
/* EIN FRAGMENT, WEIL `reihe.before(...)` HIER NICHTS TUT.
|
||
Die Reihe haengt in diesem Moment noch an keinem Elternknoten --
|
||
sie wird erst vom Aufrufer eingesetzt --, und `before()` auf
|
||
einem heimatlosen Element ist wirkungslos. Ohne Fehlermeldung:
|
||
Der Hinweis wurde gebaut, stand nirgends, und das Bild sah aus
|
||
wie vorher. Aufgefallen beim Nachsehen am Bildschirmfoto, nicht
|
||
im Code. */
|
||
if (!hinweis) return reihe;
|
||
const beides = document.createDocumentFragment();
|
||
beides.append(hinweis, reihe);
|
||
return beides;
|
||
}
|
||
|
||
async function aendern(m, stand, antwort) {
|
||
melde('eingang-fehler', '');
|
||
try {
|
||
const a = await hole(`/workspace/api/support/${m.id}`, {
|
||
method: 'PATCH',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ stand, antwort }),
|
||
});
|
||
if (!a.ok) {
|
||
melde('eingang-fehler', await fehlerText(a, 'Das hat nicht geklappt.'));
|
||
return;
|
||
}
|
||
await laden();
|
||
} catch { /* umgeleitet */ }
|
||
}
|
||
|
||
/* ===================================================================
|
||
ZEICHNEN
|
||
=================================================================== */
|
||
|
||
function bilanzZeichnen() {
|
||
const ziel = $('bilanz');
|
||
ziel.textContent = '';
|
||
const zahl = (stand) => meldungen.filter((m) => m.stand === stand).length;
|
||
for (const [stand, wort] of [
|
||
/* DER VIERTE STAND BEKOMMT SEINE EIGENE ZAHL (25.09.2026).
|
||
|
||
Ohne sie zaehlte „wartet" nirgends mit -- die drei Kaesten
|
||
oben ergaeben nicht mehr die Gesamtzahl, und Meldungen, die
|
||
auf eine Bestaetigung warten, waeren unsichtbar. Genau die
|
||
Sorte Zustand, die man beim Zaehlen vergisst. */
|
||
['neu', 'neu'], ['in_arbeit', 'in Arbeit'],
|
||
['wartet', 'wartet auf Melder'], ['erledigt', 'erledigt'],
|
||
]) {
|
||
const kasten = el('div', 's-bilanz__feld');
|
||
kasten.dataset.stand = stand;
|
||
/* OB DAS FELD LEER IST, steht als Merkmal daran -- das Stilblatt
|
||
faerbt die "neu"-Null damit nicht mehr rot. Gerechnet wird es
|
||
hier und nicht im CSS: Dort gibt es keine Zahlen, nur Text,
|
||
und ":empty" traefe eine "0" nicht. */
|
||
const n = zahl(stand);
|
||
if (!n) kasten.dataset.leer = 'ja';
|
||
kasten.append(el('span', 's-bilanz__zahl', String(n)));
|
||
kasten.append(el('span', 's-bilanz__wort', wort));
|
||
ziel.append(kasten);
|
||
}
|
||
}
|
||
|
||
function filterZeichnen() {
|
||
const ziel = $('filter');
|
||
ziel.textContent = '';
|
||
const zaehl = (welche) => welche === 'alle'
|
||
? meldungen.length
|
||
: welche === 'offen'
|
||
? meldungen.filter((m) => m.stand !== 'erledigt').length
|
||
: meldungen.filter((m) => m.stand === welche).length;
|
||
for (const [wert, wort] of [
|
||
['offen', 'Offen'], ['erledigt', 'Erledigt'], ['alle', 'Alle'],
|
||
]) {
|
||
const k = el('button', 's-filter__knopf');
|
||
k.type = 'button';
|
||
k.dataset.wert = wert;
|
||
k.setAttribute('aria-pressed', String(filter === wert));
|
||
k.append(el('span', null, wort));
|
||
k.append(el('span', 's-filter__zahl', String(zaehl(wert))));
|
||
k.addEventListener('click', () => { filter = wert; listeZeichnen(); });
|
||
ziel.append(k);
|
||
}
|
||
}
|
||
|
||
function listeZeichnen() {
|
||
if (leitung) {
|
||
filterZeichnen();
|
||
const ziel = $('eingang-liste');
|
||
ziel.textContent = '';
|
||
const drin = meldungen.filter((m) => filter === 'alle' ? true
|
||
: filter === 'offen' ? m.stand !== 'erledigt' : m.stand === filter);
|
||
if (!drin.length) {
|
||
/* EIN LEERER EINGANG IST EINE GUTE NACHRICHT und soll auch so
|
||
aussehen — nicht wie eine Seite, die nicht fertig geladen
|
||
hat. */
|
||
ziel.append(el('p', 's-leer', filter === 'offen'
|
||
? 'Nichts offen. Alles abgearbeitet.'
|
||
: 'Hier steht nichts.'));
|
||
return;
|
||
}
|
||
for (const m of drin) ziel.append(karteBauen(m));
|
||
return;
|
||
}
|
||
|
||
const block = $('meine-block');
|
||
const ziel = $('meine-liste');
|
||
ziel.textContent = '';
|
||
$('meine-zahl').textContent = String(meldungen.length);
|
||
block.hidden = meldungen.length === 0;
|
||
for (const m of meldungen) ziel.append(karteBauen(m));
|
||
}
|
||
|
||
async function laden() {
|
||
try {
|
||
const a = await hole('/workspace/api/support');
|
||
if (!a.ok) { melde('eingang-fehler', 'Die Meldungen ließen sich nicht laden.'); return; }
|
||
const r = await a.json();
|
||
leitung = r.leitung === true;
|
||
meldungen = Array.isArray(r.meldungen) ? r.meldungen : [];
|
||
|
||
/* DIE SEITE STELLT SICH AUF DEN UM, DER SIE ÖFFNET. */
|
||
$('eingang-block').hidden = !leitung;
|
||
$('meine-block').hidden = leitung || meldungen.length === 0;
|
||
if (leitung) {
|
||
$('seiten-titel').textContent = 'Support';
|
||
$('seiten-unter').textContent =
|
||
'Was aus dem Team und der Community gemeldet wurde. '
|
||
+ 'Das Älteste steht oben – was am längsten wartet, gehört nach vorn.';
|
||
/* MELDEN KANN DIE LEITUNG AUCH. Filipe findet selbst Fehler,
|
||
und ein Weg, der für ihn nicht gilt, ist ein zweiter Weg,
|
||
den er dann anders benutzt. Das Feld rutscht nur nach
|
||
unten: Wer den Eingang aufmacht, will zuerst sehen, was
|
||
liegt. */
|
||
/* WIRKLICH VERSCHIEBEN, nicht nur eine Klasse setzen.
|
||
`order` wirkt nur in einem Flex- oder Rasterbehaelter, und
|
||
`.inhalt` ist keiner -- nachgemessen in start.css. Ein
|
||
`append` an denselben Elternteil bewegt den Kasten ans
|
||
Ende, ohne ihn neu zu bauen: Der Text im Feld bleibt
|
||
stehen, falls gerade jemand tippt. */
|
||
const kasten = $('melden-block');
|
||
kasten.classList.add('s-melden--unten');
|
||
kasten.parentElement?.append(kasten);
|
||
bilanzZeichnen();
|
||
}
|
||
listeZeichnen();
|
||
|
||
/* AUS EINER BENACHRICHTIGUNG KOMMT MAN MIT ?meldung=… — dann
|
||
gehört der Blick genau dorthin und nicht an den Seitenanfang. */
|
||
const wohin = new URLSearchParams(location.search).get('meldung');
|
||
if (wohin) {
|
||
const karte = document.getElementById(`meldung-${Number(wohin)}`);
|
||
if (karte) {
|
||
karte.scrollIntoView({ behavior: window.sanft?.() || 'auto', block: 'center' });
|
||
karte.dataset.hervor = 'ja';
|
||
}
|
||
}
|
||
} catch { /* umgeleitet */ }
|
||
}
|
||
|
||
meldenEinrichten();
|
||
laden();
|
||
})();
|