Filipe: „ich will diese komplette seite vom support viel
uebersichtlicher, profissioneller, jede runde soll auch immer eine
spezielle farbe haben und anders aufgestellt aufgeteilt sein damit
man einen viel besseren und krasseren ueberblick hat."
WAS VORHER DASTAND -- an einem Bild mit zwei Runden nachgesehen,
bevor eine Zeile angefasst wurde: vier Textzeilen untereinander,
alle in derselben Farbe. „RUNDE 1", Antwort, „Ging noch nicht · …",
„RUNDE 2". Bei fuenf Runden muss man die Runden ZAEHLEN, statt sie
zu sehen -- und wer spricht, stand nur als kleiner Name daneben.
VIER TEILE SIND NEU
1. EINE LEISTE OBEN. Ein Punkt je Runde, in ihrer Farbe, mit ihrer
Nummer -- und der Rand sagt, wie sie ausgegangen ist. Damit ist
„wie oft ging es schon hin und her?" eine Frage des Hinsehens.
Erst ab zwei Runden: bei einer waere sie ein Punkt neben nichts.
2. EINE ZEITACHSE statt einer Liste. Jede Runde haengt mit einem
Punkt an einer Linie in IHRER Farbe. Die Linie ist ein Rand an
der Runde selbst, nicht am Behaelter -- so traegt jede ihr
eigenes Stueck Achse.
3. ZWEI SPRECHRICHTUNGEN je Runde, als getrennte Kaesten:
„GEANTWORTET" in der Rundenfarbe, darunter „GING NOCH NICHT"
(Bernstein) oder „GEHT WIEDER" (gruen) oder „WARTET AUF
ANTWORT". Vorher standen beide Saetze als Absaetze untereinander
und sahen gleich aus; man musste lesen, um zu wissen, von wem
sie sind. Jetzt sagt es die Form.
4. DIE LETZTEN ZWEI OFFEN, aeltere hinter „3 fruehere Runden
zeigen". Die Leiste sagt ohnehin, wie viele es gab; im Wortlaut
braucht es nicht alle. Zwei und nicht eine: Die letzte sagt, wo
es steht, die vorletzte, woran es davor lag.
DIE FARBE WIRD GERECHNET, NICHT GEPFLEGT
`rundenTon(nr)` gibt 200 + ((nr-1) mod 4) * 32 Grad. Eine Liste mit
fuenf Farben waere die naheliegende Loesung und die falsche: Runde
sechs bekaeme keine. Diese Sorte Liste hat im Haus schon dreimal
etwas gekostet.
DER BEREICH IST ENG UND ABSICHTLICH: 200 bis 296 Grad, Blau ueber
Indigo nach Violett -- die Palette der Seite (babyblau mit lila) und
der Bereich, in dem KEINE Farbe nach „gut" oder „schlecht" aussieht.
Gruen und Bernstein sind fuer den AUSGANG reserviert; waere eine
Rundenfarbe rot, stuenden zwei Aussagen in einer Farbe.
AUGENSCHONEND: 52 % Saettigung, 64 % Helligkeit -- fuer alle Toene
gleich, und nur im CSS. Sonst waere Runde drei blasser als Runde
eins, und augenschonend ist das Gegenteil von zufaellig. Kein Neon,
kein Gluehen. Die Farbe ordnet Zeilen zu; sie schreit nicht.
UND DIE FARBE ALLEIN TRAEGT NICHTS: Die Nummer steht daneben, der
Ausgang in Worten. Fuer Vorleseprogramme gibt es statt elf Punkten
einen Satz.
EIN FUND UNTERWEGS: DIE MESSUNG MASS SEIT ZWEI TAGEN NICHTS
`mess-support-runde.mjs` schickte die Rueckmeldung noch als JSON.
Am 02.10. ist die Route auf den Kopf-Weg umgestellt worden
(`x-geht`, `x-text`); seither antwortete sie mit 400. Gemerkt hat es
niemand, weil diese Datei Bilder macht und keine Rueckgabewerte
prueft: Auf dem Bild stand danach EINE Runde statt vier, und das
sieht aus wie ein Ergebnis. Aufgefallen ist es erst, als die Bilder
vier Rundenfarben zeigen sollten.
Eine Messung, die nach einem Umbau stillschweigend etwas anderes
misst, ist schlimmer als keine -- man glaubt ihr. Sie meldet den
Fehlschlag jetzt mit Code und Text.
GEPRUEFT -- pruef-support-bilder 39 -> 52 ok
die Leiste zeigt jede Runde (5 Punkte) · mit ihrer Nummer
die ersten vier Runden haben vier verschiedene Farben (4)
und die fuenfte faengt die Reihe wieder von vorn an
Gegenprobe: sie sind nicht alle gleich (4 Farben)
auch die Rundennummern tragen ihre Farbe (2 von 2)
offen stehen die letzten zwei Runden (2)
und die aelteren sind einen Griff entfernt
jede Runde zeigt beide Seiten
aufgeklappt stehen alle da (5) · und wieder zu
auf 390 px ragt nichts heraus (0 px)
bei einer einzigen Runde gibt es keine Leiste (0)
Die Gegenprobe „sie sind nicht alle gleich" ist die wichtigste:
Waere `--ton` nicht angekommen, waeren alle Punkte grau -- und „vier
Punkte" waere gruen gewesen, ohne dass eine Farbe zu sehen ist.
Gemessen wird die ANGEZEIGTE Farbe, nicht die Zahl im Attribut.
Dazu gruen: pruef-support 104 · pruef-css-klassen · pruef-zeichen 7 ·
pruef-fingermass 5.
Eine Schriftgroesse musste nachgebessert werden: .7rem sind 11,2 px,
und unter 11,5 px faengt im Haus die Grenze an, ab der man
zusammenkneift.
NUR DAS AGENTURHAUS: Die Supportseite liegt unter `/workspace`.
Co-Authored-By: Claude Opus 5 <[email protected]>
975 lines
40 KiB
JavaScript
975 lines
40 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';
|
||
/* ==== BIS ZU DREI BILDER (02.10.2026) ==========================
|
||
|
||
Hier stand `let gewaehltesBild = null` — eine Variable für genau
|
||
ein Bild, und jede neue Auswahl überschrieb sie. Das war VanVans
|
||
zweite Meldung dazu, wörtlich: „wenn man es nacheinander versucht
|
||
hinzuzufügen wird das Bild immer nur ersetzt."
|
||
|
||
JETZT EINE LISTE, und `bilderMax` kommt vom Server. Eine feste 3
|
||
an dieser Stelle wäre die Zahl, die beim Ändern vergessen wird —
|
||
dasselbe Muster wie die 380 px vom 06.09. */
|
||
let gewaehlteBilder = [];
|
||
let bilderMax = 3;
|
||
/* Welche Meldungen ihren GANZEN Verlauf zeigen. Ein Set und keine
|
||
einzelne Nummer: Es koennen mehrere Karten gleichzeitig offen
|
||
sein, und wer zwei vergleicht, will nicht, dass sich die erste
|
||
schliesst, sobald er die zweite aufmacht.
|
||
Nur Anzeige, deshalb hier und nicht am Server -- beim naechsten
|
||
Laden ist wieder zu, und das ist richtig so. */
|
||
const verlaufGanz = new Set();
|
||
|
||
/* 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
|
||
=================================================================== */
|
||
|
||
/** Die Auswahl neu zeichnen — Name, Knöpfe, Vorschaubilder.
|
||
*
|
||
* JEDES BILD HAT SEIN EIGENES KREUZ. Wer versehentlich das falsche
|
||
* dritte Bild erwischt, soll nicht alle drei neu wählen müssen;
|
||
* genau dort hört man sonst auf und schickt eben nur eines.
|
||
*
|
||
* DIE ADRESSEN WERDEN WIEDER FREIGEGEBEN (`revokeObjectURL`).
|
||
* Ohne das hält jede Vorschau ihr Bild bis zum Neuladen der Seite
|
||
* im Arbeitsspeicher fest — bei drei Handyfotos sind das schnell
|
||
* fünfzehn Megabyte, die niemand mehr braucht. */
|
||
function bilderZeigen() {
|
||
const name = $('bild-name');
|
||
const weg = $('bild-weg');
|
||
const vorschau = $('vorschau');
|
||
|
||
/* Erst die alten Adressen zurückgeben, dann neu bauen. */
|
||
for (const img of vorschau.querySelectorAll('img[data-url]')) {
|
||
URL.revokeObjectURL(img.dataset.url);
|
||
}
|
||
vorschau.textContent = '';
|
||
|
||
if (!gewaehlteBilder.length) {
|
||
name.textContent = '';
|
||
weg.hidden = true;
|
||
vorschau.hidden = true;
|
||
return;
|
||
}
|
||
|
||
const kb = gewaehlteBilder.reduce((s, d) => s + d.size, 0) / 1024;
|
||
name.textContent = gewaehlteBilder.length === 1
|
||
? `${gewaehlteBilder[0].name} · ${Math.round(kb)} KB`
|
||
: `${gewaehlteBilder.length} von ${bilderMax} Bildern · ${Math.round(kb)} KB`;
|
||
weg.hidden = false;
|
||
|
||
for (const [i, datei] of gewaehlteBilder.entries()) {
|
||
const kasten = el('div', 's-vorschau__eins');
|
||
/* 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 = el('img');
|
||
img.dataset.url = url;
|
||
img.src = url;
|
||
img.alt = `Vorschau von ${datei.name}`;
|
||
kasten.append(img);
|
||
|
||
const raus = el('button', 's-vorschau__weg', '✕');
|
||
raus.type = 'button';
|
||
raus.title = `${datei.name} wieder entfernen`;
|
||
raus.setAttribute('aria-label', `${datei.name} wieder entfernen`);
|
||
raus.addEventListener('click', () => {
|
||
gewaehlteBilder.splice(i, 1);
|
||
bilderZeigen();
|
||
});
|
||
kasten.append(raus);
|
||
vorschau.append(kasten);
|
||
}
|
||
vorschau.hidden = false;
|
||
}
|
||
|
||
/** Neu gewählte Dateien DAZULEGEN statt ersetzen.
|
||
*
|
||
* ==== VANVANS ZWEITE MELDUNG (02.10.2026) =======================
|
||
*
|
||
* „wenn man es nacheinander versucht hinzuzufügen wird das Bild
|
||
* immer nur ersetzt."
|
||
*
|
||
* `multiple` am Feld allein löst das NICHT: Auch mit `multiple`
|
||
* ersetzt jede neue Auswahl die `files`-Liste vollständig. Auf dem
|
||
* Handy wählt man aber selten drei Bilder in einem Rutsch — man
|
||
* tippt dreimal. Deshalb wird hier angehängt.
|
||
*
|
||
* DOPPELTE WERDEN NICHT ZWEIMAL GENOMMEN. Wer das Feld zweimal
|
||
* öffnet und zweimal dasselbe Foto antippt, hat eines gemeint.
|
||
* Erkannt an Name, Größe und Änderungszeit — zwei verschiedene
|
||
* Bildschirmfotos stimmen in allen dreien nicht überein. */
|
||
function bilderDazu(neue) {
|
||
let abgelehnt = '';
|
||
for (const datei of neue) {
|
||
if (gewaehlteBilder.length >= bilderMax) {
|
||
abgelehnt = `Mehr als ${bilderMax} Bilder gehen nicht – nimm die wichtigsten.`;
|
||
break;
|
||
}
|
||
/* 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) {
|
||
abgelehnt = `„${datei.name}“ ist größer als 12 MB – bitte ein kleineres.`;
|
||
continue;
|
||
}
|
||
const schon = gewaehlteBilder.some((d) => d.name === datei.name
|
||
&& d.size === datei.size && d.lastModified === datei.lastModified);
|
||
if (schon) continue;
|
||
gewaehlteBilder.push(datei);
|
||
}
|
||
melde('melden-fehler', abgelehnt);
|
||
bilderZeigen();
|
||
}
|
||
|
||
function meldenEinrichten() {
|
||
$('bild-waehlen').addEventListener('click', () => $('bild').click());
|
||
$('bild-weg').addEventListener('click', () => {
|
||
$('bild').value = '';
|
||
gewaehlteBilder = [];
|
||
bilderZeigen();
|
||
});
|
||
$('bild').addEventListener('change', (e) => {
|
||
bilderDazu([...(e.target.files || [])]);
|
||
/* DAS FELD WIRD GELEERT, die Liste nicht. Sonst meldet der
|
||
Browser beim zweiten Antippen DESSELBEN Bildes kein
|
||
`change` — er sieht keine Änderung — und es sähe aus, als
|
||
wäre der Knopf kaputt. */
|
||
e.target.value = '';
|
||
});
|
||
|
||
$('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, die Bilder im Rumpf — derselbe Weg
|
||
wie bei Chat-Anhängen. Ein Weg für beides, damit es keinen
|
||
Zustand „Meldung da, Bild fehlt noch" gibt.
|
||
Was `x-bilder` bedeutet, steht in workspace-support.js bei
|
||
`rumpfZerlegen`. */
|
||
const kopf = {
|
||
'x-text': encodeURIComponent(text),
|
||
'x-seite': location.pathname,
|
||
/* ==== DIESE ZEILE IST PFLICHT (02.10.2026) ==============
|
||
|
||
Hier stand gar kein Typ. Das ging gut, solange der Rumpf
|
||
eine EINZELNE Datei war: Ein `File` bringt seinen Typ
|
||
mit, und `fetch` setzt daraus den Kopf. Ein `Blob` aus
|
||
mehreren Dateien hat KEINEN Typ -- dann schickt `fetch`
|
||
die Zeile gar nicht, `express.raw` fuehlt sich mit
|
||
seinem Sternchen-Muster nicht zustaendig (ohne Kopf
|
||
passt kein Muster), und der Server bekommt einen leeren
|
||
Rumpf.
|
||
|
||
(Das Muster steht hier absichtlich NICHT woertlich da:
|
||
Ein Sternchen mit Schraegstrich beendet diesen
|
||
Kommentar. Genau daran ist der erste Versuch dieser
|
||
Zeile gescheitert -- die Seite lud danach gar nicht
|
||
mehr, und die Pruefung hat es in derselben Minute
|
||
gemeldet.)
|
||
|
||
FOLGE WAERE GEWESEN: Die Meldung geht durch, der Text
|
||
kommt an, die Bilder sind weg -- ohne Fehler, ohne
|
||
Meldung. Genau das, was VanVan gemeldet hat, nur
|
||
schlimmer. Die Pruefungen am Server waren dabei gruen;
|
||
gefunden hat es erst `pruef-support-bilder` im echten
|
||
Browser. */
|
||
'Content-Type': 'application/octet-stream',
|
||
};
|
||
if (gewaehlteBilder.length) {
|
||
kopf['x-bilder'] = gewaehlteBilder.map((d) => d.size).join(',');
|
||
}
|
||
const a = await hole('/workspace/api/support', {
|
||
method: 'POST',
|
||
headers: kopf,
|
||
body: new Blob(gewaehlteBilder),
|
||
});
|
||
if (!a.ok) {
|
||
melde('melden-fehler', await fehlerText(a, 'Das hat nicht geklappt.'));
|
||
return;
|
||
}
|
||
$('text').value = '';
|
||
$('bild').value = '';
|
||
gewaehlteBilder = [];
|
||
bilderZeigen();
|
||
$('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
|
||
=================================================================== */
|
||
|
||
/** Welchen Farbton hat Runde n?
|
||
*
|
||
* ==== GERECHNET, NICHT GEPFLEGT (03.10.2026) ====================
|
||
*
|
||
* Filipe: „jede runde soll auch immer eine spezielle farbe haben."
|
||
*
|
||
* Eine Liste mit fuenf Farben waere die naheliegende Loesung und
|
||
* die falsche: Runde sechs bekaeme keine. Im Haus hat genau diese
|
||
* Sorte Liste schon dreimal etwas gekostet (die 380-px-Schwelle,
|
||
* die abgeschriebenen Spalten, der Kachelsatz). Eine Formel kann
|
||
* nicht veralten.
|
||
*
|
||
* DER BEREICH IST ABSICHTLICH ENG: 200 bis 296 Grad, also Blau
|
||
* ueber Indigo nach Violett. Das ist die Palette der Seite
|
||
* (babyblau mit lila, Filipes Wunsch vom 25.09.) -- und es ist der
|
||
* Bereich, in dem KEINE Farbe nach „gut" oder „schlecht" aussieht.
|
||
* Gruen und Bernstein sind fuer den AUSGANG reserviert („Geht
|
||
* wieder" / „Ging noch nicht"); waere eine Rundenfarbe rot,
|
||
* stuenden zwei Aussagen in einer Farbe.
|
||
*
|
||
* VIER TOENE, DANN VON VORN. Mehr als vier Schritte in diesem
|
||
* engen Bereich waeren nicht mehr auseinanderzuhalten -- und die
|
||
* Nummer steht ohnehin daneben. Farbe allein traegt hier keine
|
||
* Aussage; sie hilft dem Auge, Zeilen zuzuordnen.
|
||
*
|
||
* SAETTIGUNG UND HELLIGKEIT STEHEN IM CSS und sind fuer alle
|
||
* Toene gleich. Nur so ist der Kontrast bei jeder Runde derselbe
|
||
* -- sonst waere Runde drei blasser als Runde eins, und
|
||
* augenschonend ist das Gegenteil von zufaellig. */
|
||
function rundenTon(nr) {
|
||
const n = Number(nr);
|
||
const i = (Number.isFinite(n) && n > 0 ? n - 1 : 0) % 4;
|
||
return 200 + i * 32;
|
||
}
|
||
|
||
|
||
/** Die Bilder einer Meldung oder einer Antwortrunde anzeigen.
|
||
*
|
||
* ==== EINE FUNKTION FÜR BEIDE STELLEN (02.10.2026) ==============
|
||
*
|
||
* Hier stand `bildAnhaengen(karte, m)` für das eine Bild der
|
||
* Meldung, und weiter unten noch einmal fast derselbe Block für
|
||
* das Bild einer Runde. Mit bis zu drei Bildern wären daraus zwei
|
||
* Schleifen geworden, die dasselbe tun und beim nächsten Mal nur
|
||
* an einer Stelle verbessert werden.
|
||
*
|
||
* Der Server schickt an beiden Stellen dieselbe Form (`bilder`:
|
||
* Liste aus `{ weg, breite, hoehe }`), deshalb genügt eine
|
||
* Funktion und ein Alternativtext, der sagt, wo man ist. */
|
||
function bilderAnhaengen(wohin, bilder, wortDazu) {
|
||
const liste = Array.isArray(bilder) ? bilder : [];
|
||
if (!liste.length) return;
|
||
const kasten = el('div', 's-karte__bild');
|
||
if (liste.length > 1) kasten.dataset.viele = 'ja';
|
||
for (const [i, b] of liste.entries()) {
|
||
const a = el('a');
|
||
a.href = b.weg;
|
||
a.target = '_blank';
|
||
a.rel = 'noopener noreferrer';
|
||
a.title = 'In voller Größe ansehen';
|
||
const img = el('img');
|
||
img.src = b.weg;
|
||
img.alt = liste.length > 1
|
||
? `${wortDazu} – Bild ${i + 1} von ${liste.length}`
|
||
: wortDazu;
|
||
img.loading = 'lazy';
|
||
img.decoding = 'async';
|
||
/* 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 (b.breite > 0 && b.hoehe > 0) { img.width = b.breite; img.height = b.hoehe; }
|
||
a.append(img);
|
||
kasten.append(a);
|
||
}
|
||
wohin.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));
|
||
bilderAnhaengen(k, m.bilder, 'Bild zur Meldung');
|
||
|
||
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) {
|
||
/* ==== DER VERLAUF ALS ZEITACHSE (03.10.2026) ================
|
||
|
||
Filipe: „ich will diese komplette seite vom support viel
|
||
uebersichtlicher, profissioneller, jede runde soll auch immer
|
||
eine spezielle farbe haben und anders aufgestellt aufgeteilt
|
||
sein damit man einen viel besseren und krasseren ueberblick
|
||
hat."
|
||
|
||
WAS VORHER DASTAND (gemessen an einem Bild mit zwei Runden):
|
||
vier Textzeilen untereinander, alle in derselben Farbe --
|
||
„RUNDE 1", Antwort, „Ging noch nicht · …", „RUNDE 2". Bei
|
||
fuenf Runden ist das eine Wand, in der man die Runden zaehlen
|
||
muss, statt sie zu sehen.
|
||
|
||
DREI DINGE SIND NEU:
|
||
|
||
1. EINE LEISTE OBEN, die alle Runden auf einen Blick zeigt --
|
||
ein Punkt je Runde, in ihrer Farbe, mit ihrem Ausgang.
|
||
Damit ist „wie oft ging es schon hin und her?" eine Frage
|
||
des Hinsehens.
|
||
|
||
2. EINE ZEITACHSE statt einer Liste: links eine Linie, je
|
||
Runde ein Punkt darauf. Man sieht die Abfolge, nicht nur
|
||
die Reihenfolge.
|
||
|
||
3. ZWEI SPRECHRICHTUNGEN je Runde, deutlich getrennt: was
|
||
die Leitung getan hat, und was der Melder daraufhin
|
||
gesagt hat. Vorher stand beides als Absatz untereinander
|
||
und sah gleich aus.
|
||
|
||
DIE FARBE WIRD GERECHNET, NICHT GEPFLEGT. Eine Liste mit
|
||
fuenf Farben waere die Stelle, an der Runde sechs keine mehr
|
||
bekommt -- und im Haus hat genau diese Sorte Liste schon
|
||
dreimal etwas gekostet. Der Farbton ergibt sich aus der
|
||
Rundennummer (siehe `rundenTon`). */
|
||
const liste = el('div', 's-verlauf');
|
||
|
||
/* DIE LEISTE -- nur ab zwei Runden. Bei einer waere sie ein
|
||
Punkt neben einem Punkt und sagte nichts. */
|
||
if (verlauf.length > 1) {
|
||
const leiste = el('div', 's-rundenleiste');
|
||
leiste.setAttribute('aria-hidden', 'true');
|
||
for (const r of verlauf) {
|
||
const p = el('span', 's-rundenleiste__punkt', String(r.nr));
|
||
p.style.setProperty('--ton', String(rundenTon(r.nr)));
|
||
if (r.urteil) p.dataset.urteil = r.urteil;
|
||
leiste.append(p);
|
||
}
|
||
liste.append(leiste);
|
||
/* FUER VORLESEPROGRAMME EIN SATZ statt elf Punkte. Die Leiste
|
||
ist ein Bild; was sie sagt, steht hier in Worten. */
|
||
const offen = verlauf.filter((r) => !r.urteil).length;
|
||
liste.append(el('p', 's-nurlesen',
|
||
`${verlauf.length} Runden`
|
||
+ (offen ? `, die letzte wartet noch auf eine Antwort` : '')));
|
||
}
|
||
|
||
/* ==== DIE LETZTEN ZWEI OFFEN (03.10.2026) ==================
|
||
|
||
Filipe: „damit man einen viel besseren und krasseren
|
||
ueberblick hat."
|
||
|
||
Die Leiste oben sagt, WIE VIELE Runden es gab. Darunter
|
||
braucht es nicht alle im Wortlaut: Bei sechs Runden sind das
|
||
zwoelf Kaesten, und die Karte ist wieder die Wand, die sie
|
||
vorher war.
|
||
|
||
ZWEI, NICHT EINE: Die letzte Runde ist das, was gerade
|
||
gilt -- die vorletzte sagt, woran es davor lag. Erst beide
|
||
zusammen erklaeren, warum es noch laeuft.
|
||
|
||
UND NICHT NULL: Wer die Karte oeffnet, will lesen, wo es
|
||
steht, nicht erst klicken. Dieselbe Ueberlegung wie bei den
|
||
Bewerbungsantworten heute Nacht -- und dort hat sie eine
|
||
Messung bestanden: 922 px auf einem 844-px-Bildschirm waren
|
||
zu viel, drei offene Zeilen sind es nicht. */
|
||
const GANZ = verlaufGanz.has(m.id);
|
||
const RUNDEN_OFFEN = 2;
|
||
const frueher = Math.max(0, verlauf.length - RUNDEN_OFFEN);
|
||
if (frueher && !GANZ) {
|
||
const mehr = el('button', 's-verlauf__mehr');
|
||
mehr.type = 'button';
|
||
mehr.setAttribute('aria-expanded', 'false');
|
||
mehr.append(el('span', null, frueher === 1
|
||
? 'Eine frühere Runde zeigen'
|
||
: `${frueher} frühere Runden zeigen`));
|
||
mehr.append(el('span', 's-verlauf__pfeil', '▾'));
|
||
mehr.addEventListener('click', () => {
|
||
verlaufGanz.add(m.id);
|
||
listeZeichnen();
|
||
});
|
||
liste.append(mehr);
|
||
}
|
||
|
||
const sichtbar = (frueher && !GANZ) ? verlauf.slice(frueher) : verlauf;
|
||
for (const r of sichtbar) {
|
||
const runde = el('div', 's-runde');
|
||
runde.style.setProperty('--ton', String(rundenTon(r.nr)));
|
||
if (r.urteil) runde.dataset.urteil = r.urteil;
|
||
|
||
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));
|
||
if (r.am) {
|
||
const wann = el('span', 's-runde__zeit', herZeit(r.am));
|
||
wann.title = genau(r.am);
|
||
kopfR.append(wann);
|
||
}
|
||
runde.append(kopfR);
|
||
|
||
/* (1) WAS DIE LEITUNG GETAN HAT. */
|
||
const vonOben = el('div', 's-schritt');
|
||
vonOben.dataset.wer = 'leitung';
|
||
vonOben.append(el('span', 's-schritt__schild', 'Geantwortet'));
|
||
vonOben.append(el('p', 's-schritt__text', r.antwort));
|
||
runde.append(vonOben);
|
||
|
||
/* (2) WAS DER MELDER DARAUFHIN GESAGT HAT -- und die Bilder
|
||
dazu gehoeren in DIESEN Kasten, nicht unter die Runde:
|
||
Sie sind sein Beleg, nicht ihrer. */
|
||
if (r.urteil) {
|
||
const vonUnten = el('div', 's-schritt');
|
||
vonUnten.dataset.wer = 'melder';
|
||
vonUnten.dataset.urteil = r.urteil;
|
||
vonUnten.append(el('span', 's-schritt__schild',
|
||
r.urteil === 'geht' ? 'Geht wieder' : 'Ging noch nicht'));
|
||
if (r.urteil_text) vonUnten.append(el('p', 's-schritt__text', r.urteil_text));
|
||
bilderAnhaengen(vonUnten, r.bilder,
|
||
'Bildschirmfoto zur Antwort in Runde ' + r.nr);
|
||
runde.append(vonUnten);
|
||
} else {
|
||
/* NOCH KEINE ANTWORT -- und das wird gesagt, statt die
|
||
Zeile weglassen. Eine Runde ohne Rueckmeldung sieht sonst
|
||
aus wie eine, bei der etwas fehlt. */
|
||
const warte = el('div', 's-schritt');
|
||
warte.dataset.wer = 'warte';
|
||
warte.append(el('span', 's-schritt__schild', 'Wartet auf Antwort'));
|
||
runde.append(warte);
|
||
bilderAnhaengen(runde, r.bilder,
|
||
'Bildschirmfoto zur Antwort in Runde ' + r.nr);
|
||
}
|
||
liste.append(runde);
|
||
}
|
||
/* UND WIEDER ZU. Ein Knopf, der nur aufklappt, ist ein
|
||
Einwegschalter -- und das faellt erst auf, wenn jemand
|
||
zurueck will. */
|
||
if (frueher && GANZ) {
|
||
const weniger = el('button', 's-verlauf__mehr');
|
||
weniger.type = 'button';
|
||
weniger.setAttribute('aria-expanded', 'true');
|
||
weniger.append(el('span', null, 'Nur die letzten zwei Runden'));
|
||
weniger.append(el('span', 's-verlauf__pfeil', '▴'));
|
||
weniger.addEventListener('click', () => {
|
||
verlaufGanz.delete(m.id);
|
||
listeZeichnen();
|
||
});
|
||
liste.append(weniger);
|
||
}
|
||
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.',
|
||
/* ==== EIN BILD DARF MIT (02.10.2026) ====================
|
||
|
||
VanVan: „… damit man nochmal ein Bild anhängen kann, wenn
|
||
das Problem noch besteht oder sich durch die Änderung ein
|
||
neues Problem ergeben hat."
|
||
|
||
Ihr zweiter Halbsatz ist der Grund: Beim Melden war das
|
||
Bild vom ERSTEN Problem. Taucht durch die Änderung ein
|
||
neues auf, hilft das alte Bild niemandem.
|
||
|
||
NIE PFLICHT. Wer keines hat, antwortet ohne -- ein
|
||
Bildschirmfoto ist Hilfe, keine Hürde.
|
||
|
||
SEIT DEM 02.10. BIS ZU DREI, genau wie beim Melden.
|
||
VanVan, Meldung #11: „2-3 wären besser." Dass es beim
|
||
Melden drei sein dürfen und bei der Antwort eines, wäre
|
||
dieselbe Frage mit zwei Antworten. */
|
||
bild: 'Bildschirmfotos dazu (freiwillig)',
|
||
bildMax: bilderMax,
|
||
});
|
||
if (!antwort) return;
|
||
await rueckmelden(m, false, antwort.grund || '', antwort.bilder || []);
|
||
});
|
||
reihe.append(nicht);
|
||
return reihe;
|
||
}
|
||
|
||
/** Die Antwort des Melders -- mit Bild oder ohne.
|
||
*
|
||
* DERSELBE WEG WIE BEIM MELDEN (02.10.2026): Text und Urteil
|
||
* reisen im Kopf, das Bild im Rumpf. Ein zweiter Weg nur für den
|
||
* Anhang hätte einen Zustand dazwischen -- eine Antwort, die schon
|
||
* zählt, während das Bild noch lädt.
|
||
*
|
||
* OHNE BILD IST DER RUMPF LEER, und das ist zulässig. Dann steht
|
||
* im Kopf alles, was es zu sagen gibt.
|
||
*/
|
||
async function rueckmelden(m, geht, text, bilder) {
|
||
melde('eingang-fehler', '');
|
||
const anhang = Array.isArray(bilder) ? bilder : (bilder ? [bilder] : []);
|
||
try {
|
||
const kopf = {
|
||
/* Der Typ sagt, was im RUMPF liegt. Bei mehreren Bildern ist
|
||
das kein einzelnes Bildformat mehr, sondern eine Folge --
|
||
`application/octet-stream` ist dafür die ehrlichste
|
||
Auskunft, und der Server erkennt jedes Stück ohnehin an
|
||
seinen ersten Bytes statt an dieser Zeile. */
|
||
'Content-Type': 'application/octet-stream',
|
||
'x-geht': geht ? 'ja' : 'nein',
|
||
/* Prozentkodiert, weil ein Kopf keine Umlaute trägt -- und
|
||
„Grüße" in einer Begründung ist der Normalfall, nicht die
|
||
Ausnahme. Genauso macht es das Melden. */
|
||
'x-text': encodeURIComponent(text || ''),
|
||
};
|
||
if (anhang.length) kopf['x-bilder'] = anhang.map((d) => d.size).join(',');
|
||
const a = await hole(`/workspace/api/support/${m.id}/rueckmeldung`, {
|
||
method: 'POST',
|
||
headers: kopf,
|
||
body: new Blob(anhang),
|
||
});
|
||
/* 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 : [];
|
||
/* WIE VIELE BILDER GEHEN, SAGT DER SERVER (02.10.2026). Steht
|
||
es nicht in der Antwort (eine Seite aus dem Zwischenspeicher
|
||
trifft auf einen neuen Server oder umgekehrt), bleibt der
|
||
Vorgabewert — eine fehlende Zahl darf die Auswahl nicht auf
|
||
null setzen. */
|
||
if (Number.isInteger(r.bilder_max) && r.bilder_max > 0) bilderMax = r.bilder_max;
|
||
|
||
/* 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();
|
||
})();
|