Filipes Frage: „koennen die leute auch schon mehrere fotos schicken wenn die im support was melden wollen?" Technisch seit dem 02.10. (VanVans Meldung #11), und heute mehrfach am laufenden Server nachgemessen. Beim Nachsehen auf der SEITE stand aber: Knopf: „Bild anhängen" -- Einzahl Unterzeile: „häng ein Bild dran" -- Einzahl daneben: (nichts) Wer nicht zufaellig ein zweites Mal auf den Knopf tippt, schickt eines. Aus seiner Sicht waere VanVans Meldung ungeloest -- und er haette recht. EINE MOEGLICHKEIT, VON DER MAN NICHTS WEISS, GIBT ES NICHT. Das ist dieselbe Sorte wie ein Knopf, der eine Absage holt, nur andersherum: Dort verspricht die Oberflaeche zu viel, hier zu wenig. Beides kostet denselben Menschen dieselbe Zeit. GEAENDERT Knopf: „Bilder anhängen" Unterzeile: „häng Bilder dran" daneben: „bis zu 3 Bilder" -- bevor man etwas waehlt nach einem: „eins.png · 24 KB · noch 2 möglich" Die letzte Zeile ist die wichtigere von beiden: „eins.png · 24 KB" allein sagt nicht, dass ein zweites geht -- und genau an dieser Stelle hoert jemand auf. DIE ZAHL KOMMT VOM SERVER (`bilder_max`) und wird neu geschrieben, sobald sie da ist. Ohne das stuende beim ersten Laden der Vorgabewert dort -- heute zufaellig derselbe, morgen vielleicht nicht. Eine Zahl, die zufaellig stimmt, ist keine Auskunft. GEPRUEFT -- pruef-support-bilder 52 -> 56 ok der Knopf steht in der Mehrzahl („Bilder anhängen") und daneben steht, wie viele gehen („bis zu 3 Bilder") auch die Unterzeile sagt nicht mehr „ein Bild" und daneben steht, dass noch Platz ist („eins.png · 0 KB · noch 2 möglich") Dazu gruen: pruef-zeichen 7 · pruef-css-klassen. Co-Authored-By: Claude Opus 5 <[email protected]>
1000 lines
41 KiB
JavaScript
1000 lines
41 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) {
|
||
/* ==== SAGEN, DASS MEHRERE GEHEN (03.10.2026) ===============
|
||
|
||
Hier stand eine leere Zeile. Die Funktion kann seit dem
|
||
02.10. drei Bilder -- VanVans Meldung #11 -- nur erfuhr es
|
||
niemand: Der Knopf hiess „Bild anhaengen", die Unterzeile
|
||
sagte „haeng ein Bild dran", und daneben stand nichts.
|
||
|
||
Eine Moeglichkeit, von der man nichts weiss, gibt es nicht.
|
||
Das ist dieselbe Sorte wie ein Knopf, der eine Absage holt
|
||
-- nur andersherum.
|
||
|
||
DIE ZAHL KOMMT VOM SERVER (`bilderMax`). Eine feste 3 hier
|
||
waere die zweite, die beim Aendern vergessen wird. */
|
||
name.textContent = `bis zu ${bilderMax} Bilder`;
|
||
weg.hidden = true;
|
||
vorschau.hidden = true;
|
||
return;
|
||
}
|
||
|
||
const kb = gewaehlteBilder.reduce((s, d) => s + d.size, 0) / 1024;
|
||
/* UND WENN SCHON EINES DALIEGT, STEHT DA, DASS NOCH PLATZ IST.
|
||
„eins.png · 24 KB" allein sagt nicht, dass ein zweites geht --
|
||
und genau an dieser Stelle hoert jemand auf. */
|
||
const frei = bilderMax - gewaehlteBilder.length;
|
||
const rest = frei > 0
|
||
? ` · noch ${frei} möglich`
|
||
: '';
|
||
name.textContent = gewaehlteBilder.length === 1
|
||
? `${gewaehlteBilder[0].name} · ${Math.round(kb)} KB${rest}`
|
||
: `${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;
|
||
/* UND DIE ZEILE NEU SCHREIBEN, jetzt mit der richtigen Zahl.
|
||
Ohne das stuende beim ersten Laden der Vorgabewert da --
|
||
heute zufaellig derselbe, morgen vielleicht nicht. Eine Zahl,
|
||
die zufaellig stimmt, ist keine Auskunft. */
|
||
bilderZeigen();
|
||
|
||
/* 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();
|
||
})();
|