Chat zwischen Creator, Scouts und Managern
Wunsch Filipe, 06.09.2026: *"eine kategorie chat, wo die creator mit
ihren manager nachrichten austauschen koennen, wie erinnerungen, fragen
und noch vieles mehr."* Auf Nachfrage entschieden: Zweier-Gespraeche UND
Gruppen; Manager und Scouts duerfen sich auch ohne gemeinsamen Creator
schreiben.
WER MIT WEM steht an EINER Stelle: schreibbareIds() in workspace.js.
Bewusst getrennt von einladbareIds() (Terminwahl): Dort geht es darum,
wen man zu einem Kalendereintrag dazustellen darf -- harmlos. Ein Chat
legt ein dauerhaftes Gespraech an, schickt eine Benachrichtigung und
macht den eigenen Namen sichtbar. Zwei Fragen, zwei Funktionen.
Ein Creator erreicht seine Betreuer und DogFather, KEINEN fremden
Creator. Eine Gruppe ist kein Schlupfloch: Jede Nummer wird einzeln
gegen dieselbe Regel geprueft.
SOFORT STATT NACHFRAGEN IM TAKT (SSE). Ein Chat, der alle fuenf
Sekunden fragt, ist fuenf Sekunden langsam und stellt bei zehn
Angemeldeten 7200 Anfragen in der Stunde fuer Nachrichten, die es
meistens nicht gibt. Kein WebSocket: Hier fliesst alles in eine
Richtung, und SSE uebersteht einen Verbindungsabriss von selbst.
DIE ZAHL STEHT AUF JEDER SEITE, nicht nur im Chat. Eine Nachricht, die
man erst sieht, wenn man den Chat aufmacht, ist keine Nachricht --
sie ist ein Fundstueck.
Push aufs Handy nur an die, die NICHT gerade zusehen. Wer die Seite
offen hat, sieht es ohnehin; ihm zusaetzlich etwas aufs Handy zu
schicken ist der schnellste Weg, dass er Benachrichtigungen abschaltet.
WAS BEWUSST NICHT GEHT:
* Niemand liest fremde Gespraeche mit -- auch DogFather nicht. Ein
Chat, in dem der Chef stillschweigend mitliest, ist kein Chat.
Er kann jederzeit jedem schreiben, aber sichtbar.
* Eine abgeschickte Nachricht laesst sich nicht aendern, nur
zuruecknehmen. Wer Absprachen nachtraeglich umschreiben kann, macht
den Verlauf wertlos. Zurueckgenommenes hinterlaesst einen Hinweis
statt eines Lochs.
DAZU: CALL-LISTE NACH ZEIT GEGLIEDERT
Wunsch mit Bild von "STEHT AN 27": *"die liste soll kategorisiert sein
und werden mit einem button zum auf und zu machen."* Jetzt Heute /
Diese Woche / Naechste Woche / Spaeter, offen nur der erste gefuellte
Block. Nach ZEIT und nicht nach Titel: Auf dem Bild waren fast alle
"Community-Talk" -- nach Titel gruppiert haette man zwei Ueberschriften
und darunter dieselbe lange Liste.
DER FUND, DER DIE FUNKTION GERETTET HAT: Ein zugeklappter Block zeigte
seinen Inhalt trotzdem -- 367 Pixel Hoehe bei open=false. <details>
versteckt seinen Inhalt naemlich nur, solange niemand dem Inhalt eine
eigene display-Angabe gibt, und .gruppe__karten traegt display:grid.
Der Knopf haette sich bewegt und sonst nichts getan. Im Bildschirmfoto
fiel es nicht auf, weil der Ausschnitt genau darueber endete.
Ausserdem gefunden und behoben:
* Ein Wettlauf beim Abschicken: War der Ereignisstrom schneller als
die Antwort auf das POST, stand die eigene Nachricht zweimal im
Verlauf.
* Die Chatseite war zu hoch -- die Kopfleistenhoehe stand als fester
Wert (62 px) im CSS, am Handy ist sie doppelt so hoch. Das
Eingabefeld stand halb unter dem Bildschirmrand. Jetzt gemessen.
* Der Platzhalter "Enter schickt ..." brach am Handy um UND stimmte
dort nicht: Ohne Tastatur schickt Enter absichtlich nicht.
* Der Dialog benutzte Klassen aus aufgaben.css, das chat.html nicht
lud -- gemeldet von pruef-css-klassen, bevor jemand einen
ungestylten Dialog zu sehen bekam.
Pruefungen: pruef-chat (44 Punkte, Rechte und Sichtbarkeit),
pruef-chat-optik (24, zwei echte Browser schreiben sich),
pruef-call-kategorien (16).
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -403,6 +403,55 @@
|
||||
{ schluessel: 'erledigt', titel: 'Festgehalten', frage: null },
|
||||
];
|
||||
|
||||
/* ---------- Kategorien innerhalb einer Gruppe ---------------------------
|
||||
|
||||
Wunsch Filipe, 06.09.2026, mit einem Bild von "STEHT AN 27":
|
||||
*"die liste da soll kategorisiert sein und werden mit einem button
|
||||
zum auf und zu machen."*
|
||||
|
||||
WARUM NACH ZEIT und nicht nach Titel: Auf dem Bild standen
|
||||
siebenundzwanzig Karten am Stück, fast alle "Community-Talk" --
|
||||
wöchentliche Serien. Nach Titel gruppiert hätte man zwei
|
||||
Überschriften und darunter wieder dieselbe lange Liste. Die Frage,
|
||||
die man an eine Terminliste hat, ist "was kommt als Nächstes", und
|
||||
die Antwort darauf ist ein Zeitraum.
|
||||
|
||||
WAS OFFEN IST: nur die erste Kategorie, die etwas enthält. Alles
|
||||
aufzuklappen wäre wieder die Wand von vorher; alles zuzuklappen
|
||||
hiesse, dass man nach dem Laden erst einmal klicken muss, um
|
||||
überhaupt etwas zu sehen. */
|
||||
const ZEITRAEUME = [
|
||||
{ schluessel: 'heute', titel: 'Heute' },
|
||||
{ schluessel: 'woche', titel: 'Diese Woche' },
|
||||
{ schluessel: 'naechste', titel: 'Nächste Woche' },
|
||||
{ schluessel: 'spaeter', titel: 'Später' },
|
||||
];
|
||||
|
||||
/** In welchen Zeitraum fällt dieser Termin?
|
||||
*
|
||||
* ORTSZEIT durchgehend. Ein `toISOString()` läge zwischen
|
||||
* Mitternacht und zwei Uhr einen Tag daneben -- und ausgerechnet
|
||||
* dann wäre "Heute" leer, obwohl etwas ansteht. */
|
||||
function zeitraumVon(beginn) {
|
||||
const d = new Date(String(beginn).replace(' ', 'T'));
|
||||
if (Number.isNaN(d.getTime())) return 'spaeter';
|
||||
|
||||
const heute = new Date();
|
||||
heute.setHours(0, 0, 0, 0);
|
||||
const tag = new Date(d); tag.setHours(0, 0, 0, 0);
|
||||
const tage = Math.round((tag - heute) / 86400000);
|
||||
|
||||
if (tage <= 0) return 'heute';
|
||||
/* Bis zum Ende DIESER Woche (Sonntag), nicht "in sieben Tagen".
|
||||
"Diese Woche" heisst umgangssprachlich bis Sonntag -- wer am
|
||||
Freitag "diese Woche" liest, erwartet nicht den Mittwoch darauf. */
|
||||
const wochentag = (heute.getDay() + 6) % 7; // Mo=0 … So=6
|
||||
const bisSonntag = 6 - wochentag;
|
||||
if (tage <= bisSonntag) return 'woche';
|
||||
if (tage <= bisSonntag + 7) return 'naechste';
|
||||
return 'spaeter';
|
||||
}
|
||||
|
||||
function zeichne() {
|
||||
const ziel = $('listen');
|
||||
ziel.textContent = '';
|
||||
@@ -427,13 +476,73 @@
|
||||
kopf.append(el('h2', 'stufe__titel', g.titel), el('span', 'stufe__zahl', String(liste.length)));
|
||||
s.append(kopf);
|
||||
if (g.frage) s.append(el('p', 'block__frage', g.frage));
|
||||
const box = el('div', 'gruppe__karten');
|
||||
for (const c of liste) box.append(karte(c, g.schluessel));
|
||||
s.append(box);
|
||||
|
||||
/* DIE ANSTEHENDEN WERDEN NACH ZEITRAUM UNTERTEILT (06.09.2026).
|
||||
Die anderen beiden Gruppen nicht: "Protokoll fehlt" ist eine
|
||||
Arbeitsliste, die man ganz sehen will, und "Festgehalten" ist
|
||||
ohnehin schon ein Archiv. Eine Unterteilung, die überall gleich
|
||||
aussieht, aber nur an einer Stelle etwas nützt, verwirrt an den
|
||||
anderen. */
|
||||
if (g.schluessel === 'anstehend' && liste.length > 4) {
|
||||
s.append(...zeitraumBloecke(liste, g.schluessel));
|
||||
} else {
|
||||
const box = el('div', 'gruppe__karten');
|
||||
for (const c of liste) box.append(karte(c, g.schluessel));
|
||||
s.append(box);
|
||||
}
|
||||
ziel.append(s);
|
||||
}
|
||||
}
|
||||
|
||||
/** Baut die aufklappbaren Zeitraum-Blöcke.
|
||||
*
|
||||
* <details> und nicht selbstgebaut: bringt Tastaturbedienung,
|
||||
* aria-expanded und das Verhalten der Browsersuche von sich aus mit.
|
||||
* Dasselbe Bauteil wie beim Bereich "Abgebrochen" auf dem
|
||||
* Aufgabenbrett -- wer eines kennt, kennt beide. */
|
||||
function zeitraumBloecke(liste, gruppe) {
|
||||
const nach = new Map(ZEITRAEUME.map((z) => [z.schluessel, []]));
|
||||
for (const c of liste) nach.get(zeitraumVon(c.beginn))?.push(c);
|
||||
|
||||
const raus = [];
|
||||
let ersterMitInhalt = true;
|
||||
for (const z of ZEITRAEUME) {
|
||||
const eigene = nach.get(z.schluessel);
|
||||
if (!eigene.length) continue;
|
||||
|
||||
const block = el('details', 'zeitblock');
|
||||
block.dataset.zeitraum = z.schluessel;
|
||||
/* Nur der erste gefüllte Block ist offen -- siehe Begründung
|
||||
oben bei ZEITRAEUME. */
|
||||
if (ersterMitInhalt) { block.open = true; ersterMitInhalt = false; }
|
||||
|
||||
const kopf = el('summary', 'zeitblock__kopf');
|
||||
kopf.append(el('span', 'zeitblock__name', z.titel));
|
||||
kopf.append(el('span', 'zeitblock__zahl', String(eigene.length)));
|
||||
/* Wann genau -- "Später" allein sagt nicht, ob morgen oder im
|
||||
November. Der erste Termin des Blocks beantwortet das. */
|
||||
const erster = eigene[0];
|
||||
if (erster) {
|
||||
kopf.append(el('span', 'zeitblock__ab',
|
||||
z.schluessel === 'spaeter' ? `ab ${kurzDatum(erster.beginn)}` : ''));
|
||||
}
|
||||
block.append(kopf);
|
||||
|
||||
const box = el('div', 'gruppe__karten');
|
||||
for (const c of eigene) box.append(karte(c, gruppe));
|
||||
block.append(box);
|
||||
raus.push(block);
|
||||
}
|
||||
return raus;
|
||||
}
|
||||
|
||||
/** "23. Sep" -- kurz, weil es neben einer Überschrift steht. */
|
||||
function kurzDatum(beginn) {
|
||||
const d = new Date(String(beginn).replace(' ', 'T'));
|
||||
if (Number.isNaN(d.getTime())) return '';
|
||||
return d.toLocaleDateString('de-DE', { day: 'numeric', month: 'short' });
|
||||
}
|
||||
|
||||
async function laden() {
|
||||
try {
|
||||
const a = await hole('/workspace/api/calls');
|
||||
|
||||
Reference in New Issue
Block a user