Termine, die von allein weiterlaufen -- bis man sie abstellt
Wunsch: "ich will dass ich da im kalender auch sachen machen die jede
woche jeden monat automatisiert immer weiter laufen so wie ich es
auswaehle bis ich es selber deaktiviere. aktiviere diese option fuer
jeden."
Acht Rhythmen (taeglich, werktags, woechentlich, alle zwei Wochen,
monatlich am Datum, am Monatsletzten, am n-ten Wochentag, jaehrlich),
wahlweise mit Enddatum oder ohne Ende. Fuer JEDE Rolle -- Creator und
Scouts legen sie fuer sich selbst an, mit denselben Grenzen wie beim
einzelnen Termin.
WARUM ECHTE TERMINE UND KEINE GERECHNETEN AUSPRAEGUNGEN
Der naheliegende Weg waere gewesen, die Wiederholung nur beim Anzeigen
auszurechnen. Das waere ein stiller Ausfall geworden: ACHT andere Stellen
lesen die Tabelle termine direkt -- Calls, Protokolle, Berichte,
Hinweise/Ampel, Suche, Aufgaben ("naechster Termin"), Uebersicht und die
Startseite. In all diesen Ansichten haette schlicht nichts gestanden, und
aufgefallen waere es erst Monate spaeter. Stattdessen macht ein
Nachfueller aus der Regel echte Zeilen (Horizont 180 Tage) -- jedes andere
Modul sieht sie mit, ohne dass dort eine Zeile geaendert werden musste.
Beim geplanten ICS-Abo gilt dasselbe.
KEIN ZEITGEBER. Der Nachfueller laeuft beim Serverstart, beim Anlegen und
Aendern und beim Oeffnen des Kalenders (dort hoechstens alle fuenf
Minuten). Er ist idempotent. Ein Cron waere eine weitere Sache gewesen,
die stillschweigend ausfallen kann.
WAS BEIM ABSTELLEN PASSIERT: Vergangene Termine und alles, was jemand von
Hand angefasst hat (verschoben, umbenannt, abgehakt), bleibt stehen. Nur
die unberuehrte Zukunft wird aus dem Kalender genommen. Wer einen
einzelnen Tag loescht, streicht nur diesen einen -- der Tag wird als
Ausnahme vermerkt, sonst legte der Nachfueller ihn wieder an.
ZEITUMSTELLUNG: Gerechnet wird auf reinen Datumstexten in UTC-Mittag, die
Uhrzeit wird als Text angehaengt. 18:00 bleibt dadurch 18:00 und wandert
im Oktober nicht auf 17:00.
DIE SICHTBARKEITSREGEL STEHT JETZT NUR NOCH EINMAL (workspace.js,
termineSichtbar) statt zweimal fast gleich. Zwei Fassungen waeren
auseinandergelaufen -- und dann haette eine Wiederholung jemandem etwas
gezeigt, was der einzelne Termin ihm verbirgt.
IN DER OBERFLAECHE beschriften sich die Rhythmen nach dem gewaehlten
Datum ("jeden Dienstag", "jeden Monat am letzten Dienstag") statt
abstrakt ("woechentlich"), inklusive des ehrlichen Hinweises, dass "jeden
31." den Februar auslaesst. Darunter steht der ganze Satz in Worten. Eine
neue Leiste "Laeuft von allein" zeigt alle laufenden Wiederholungen mit
Abstell-Schalter -- was von selbst weiterlaeuft, muss man sehen koennen.
GEPRUEFT: 67 Pruefungen (server/pruef-serien.mjs), darunter von Hand
nachgeschlagene Datumsangaben fuer jeden Takt (der letzte Dienstag im
Dezember 2026 ist der 29., nicht der 22.; der 29. Februar nur in
Schaltjahren) und vier GEGENPROBEN, die beweisen, dass die Pruefungen
auch "nicht in Ordnung" sagen koennen. Die Anzahl der Pruefungen steht in
der Bedingung, nicht nur im Meldetext. Bestehende Pruefungen unveraendert
gruen: Kalender 84, Sicht 48, Startansicht 133, Protokoll 52, Ampel 47,
Handy 50, Aufgabenbrett 44, Sprung 43, Personen-Loeschen 40, Uebersicht
33, Workspace-Seiten 32, Formulare 19, Betreuung 18, Grosscheck 15,
Lesbarkeit 14.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -63,9 +63,18 @@
|
||||
const ANLASS_FARBE = '#d8a13a';
|
||||
|
||||
const WOCHENTAGE_KURZ = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
|
||||
/* Sonntag zuerst -- das ist die Zählung von getUTCDay(), nicht die
|
||||
deutsche Wochenordnung. Wer hier Montag an den Anfang setzt,
|
||||
verschiebt jeden Wochentagsnamen um eins. */
|
||||
const WOCHENTAGE_LANG = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch',
|
||||
'Donnerstag', 'Freitag', 'Samstag'];
|
||||
const MONATE = ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
|
||||
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'];
|
||||
const ORDNUNG = ['', 'ersten', 'zweiten', 'dritten', 'vierten', 'fünften'];
|
||||
|
||||
let ich = null;
|
||||
let daten = null;
|
||||
let serien = [];
|
||||
let ansicht = 'monat';
|
||||
let anker = heuteText(); // Datum, um das herum angezeigt wird
|
||||
const zeigen = { call: true, termin: true, review: true, frist: true };
|
||||
@@ -126,6 +135,8 @@
|
||||
}
|
||||
|
||||
const fmt = (o) => new Intl.DateTimeFormat('de-DE', { timeZone: 'UTC', ...o });
|
||||
const tagLang = (t) =>
|
||||
fmt({ weekday: 'long', day: '2-digit', month: 'long' }).format(ausText(t));
|
||||
|
||||
/* ---------- Anlässe ------------------------------------------------------ */
|
||||
|
||||
@@ -265,6 +276,7 @@
|
||||
ort: t.ort, beschreibung: t.beschreibung,
|
||||
creator_name: t.creator_name, teilnehmer_name: t.teilnehmer_name,
|
||||
erstellt_von: t.erstellt_von, dauer_min: t.dauer_min,
|
||||
serie_id: t.serie_id || null,
|
||||
});
|
||||
}
|
||||
for (const f of roh.fristen || []) {
|
||||
@@ -312,12 +324,31 @@
|
||||
const k = el(e.ziel ? 'a' : 'div', 'k-pille' + (e.erledigt ? ' erledigt' : ''));
|
||||
if (e.ziel) k.href = e.ziel;
|
||||
k.style.setProperty('--pfarbe', e.farbe);
|
||||
k.title = `${e.titel}${e.zusatz ? ' — ' + e.zusatz : ''}`;
|
||||
k.title = `${e.titel}${e.zusatz ? ' — ' + e.zusatz : ''}`
|
||||
+ (e.serie_id ? ' — wiederholt sich' : '');
|
||||
if (e.uhr) k.append(el('span', 'k-pille__zeit', e.uhr));
|
||||
/* Das Zeichen steht im Raster VOR dem Titel, nicht dahinter: Eine
|
||||
Zelle im Monatsraster ist gut 150 px breit, der Titel wird dort
|
||||
abgeschnitten -- ein Zeichen am Ende wäre genau in der Ansicht
|
||||
unsichtbar, in der man den Rhythmus erkennen will. In der Liste
|
||||
(siehe zeile()) ist Platz, dort steht es hinter dem Titel und
|
||||
liest sich natürlicher. */
|
||||
if (e.serie_id) k.append(wiederholZeichen());
|
||||
k.append(document.createTextNode(e.titel));
|
||||
return k;
|
||||
}
|
||||
|
||||
/** Das Wiederhol-Zeichen. Einmal gebaut, überall gleich -- und mit
|
||||
* einer Beschriftung für Vorleseprogramme, die das Zeichen selbst
|
||||
* sonst als "Pfeil im Kreis" oder gar nicht vorlesen. */
|
||||
function wiederholZeichen() {
|
||||
const s = el('span', 'k-wdh-zeichen', '↻');
|
||||
s.setAttribute('role', 'img');
|
||||
s.setAttribute('aria-label', 'wiederholt sich');
|
||||
s.title = 'Gehört zu einer Wiederholung';
|
||||
return s;
|
||||
}
|
||||
|
||||
function anlassMarke(a) {
|
||||
const k = el('span', 'k-anlass' + (a.art === 'feiertag' ? ' feiertag' : ''),
|
||||
a.sym + ' ' + a.name);
|
||||
@@ -440,6 +471,174 @@
|
||||
$('monat-block').hidden = false;
|
||||
}
|
||||
|
||||
/* ---------- Wiederholungen ------------------------------------------------
|
||||
Wunsch vom 02.09.2026: Termine, die "jede Woche, jeden Monat
|
||||
automatisiert immer weiter laufen -- bis ich es selber deaktiviere".
|
||||
|
||||
Zwei Teile: die Auswahl im Formular (was soll sich wiederholen) und
|
||||
die Liste darunter (was läuft gerade und wie stelle ich es ab).
|
||||
|
||||
Die Beschriftungen der Auswahl hängen vom GEWÄHLTEN DATUM ab, nicht
|
||||
von abstrakten Wörtern: "jeden Dienstag" statt "wöchentlich", "jeden
|
||||
Monat am 2." statt "monatlich". Wer "monatlich" liest, weiß nicht,
|
||||
ob der 31. Januar im Februar ausfällt oder auf den 28. rutscht --
|
||||
hier steht es dran. Der Wortlaut ist derselbe wie auf dem Server
|
||||
(workspace-serien.js, taktText); die Liste unten zeigt IMMER die
|
||||
Fassung des Servers, damit im Zweifel eine von beiden gilt. */
|
||||
|
||||
function taktLage(tagText) {
|
||||
const d = ausText(tagText);
|
||||
const j = d.getUTCFullYear(), m = d.getUTCMonth(), tag = d.getUTCDate();
|
||||
const letzter = new Date(Date.UTC(j, m + 1, 0, 12)).getUTCDate();
|
||||
return {
|
||||
tag, monat: m, wochentag: d.getUTCDay(),
|
||||
nter: Math.ceil(tag / 7),
|
||||
letzterWochentag: tag + 7 > letzter,
|
||||
};
|
||||
}
|
||||
|
||||
/** Die möglichen Rhythmen für ein bestimmtes Startdatum, jeweils mit
|
||||
* dem Satz, der davon in der Vorschau steht. */
|
||||
function taktListe(tagText) {
|
||||
const l = taktLage(tagText);
|
||||
const wt = WOCHENTAGE_LANG[l.wochentag];
|
||||
return [
|
||||
{ wert: 'taeglich', text: 'jeden Tag' },
|
||||
{ wert: 'werktags', text: 'jeden Werktag (Mo–Fr)' },
|
||||
{ wert: 'woechentlich', text: `jeden ${wt}` },
|
||||
{ wert: 'zweiwoechentlich', text: `alle zwei Wochen, ${wt}s` },
|
||||
{
|
||||
wert: 'monatlich_datum',
|
||||
text: `jeden Monat am ${l.tag}.`,
|
||||
/* Ehrlich bleiben: Ein 29./30./31. gibt es nicht in jedem Monat.
|
||||
Ausgelassen wird er dann, nicht vorgezogen -- und das gehört
|
||||
in die Auswahl, nicht in eine Fußnote, die niemand liest. */
|
||||
hinweis: l.tag > 28 ? ` (Monate ohne den ${l.tag}. fallen aus)` : '',
|
||||
},
|
||||
{ wert: 'monatlich_letzter', text: 'jeden Monat am letzten Tag' },
|
||||
{
|
||||
wert: 'monatlich_wochentag',
|
||||
text: l.letzterWochentag
|
||||
? `jeden Monat am letzten ${wt}`
|
||||
: `jeden Monat am ${ORDNUNG[l.nter]} ${wt}`,
|
||||
},
|
||||
{ wert: 'jaehrlich', text: `jedes Jahr am ${l.tag}. ${MONATE[l.monat]}` },
|
||||
];
|
||||
}
|
||||
|
||||
/** Füllt die Auswahl neu -- bei jedem Datumswechsel, denn "jeden
|
||||
* Dienstag" wird zu "jeden Mittwoch", sobald man das Datum ändert.
|
||||
* Die getroffene Wahl bleibt dabei erhalten. */
|
||||
function taktAuswahlFuellen() {
|
||||
const feld = $('f-takt');
|
||||
const tag = ($('f-beginn').value || '').slice(0, 10);
|
||||
const gewaehlt = feld.value;
|
||||
feld.replaceChildren();
|
||||
const leer = document.createElement('option');
|
||||
leer.value = ''; leer.textContent = 'Einmalig – wiederholt sich nicht';
|
||||
feld.append(leer);
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(tag)) return;
|
||||
for (const t of taktListe(tag)) {
|
||||
const o = document.createElement('option');
|
||||
o.value = t.wert;
|
||||
o.textContent = t.text + (t.hinweis || '');
|
||||
feld.append(o);
|
||||
}
|
||||
feld.value = gewaehlt;
|
||||
/* War der alte Wert nicht mehr dabei, fällt die Auswahl auf
|
||||
"Einmalig" zurück -- lieber nichts wiederholen als etwas
|
||||
anderes als angezeigt. */
|
||||
if (feld.value !== gewaehlt) feld.value = '';
|
||||
wdhVorschau();
|
||||
}
|
||||
|
||||
/** Der Satz unter der Auswahl: was gleich entsteht, in Worten. */
|
||||
function wdhVorschau() {
|
||||
const takt = $('f-takt').value;
|
||||
$('feld-wdh').hidden = !takt;
|
||||
if (!takt) { $('wdh-satz').textContent = ''; return; }
|
||||
const tag = ($('f-beginn').value || '').slice(0, 10);
|
||||
const uhr = ($('f-beginn').value || '').slice(11, 16);
|
||||
const eintrag = taktListe(tag).find((t) => t.wert === takt);
|
||||
const ende = $('f-ende').value;
|
||||
$('wdh-satz').textContent = 'Läuft ' + (eintrag ? eintrag.text : takt)
|
||||
+ (uhr ? ` um ${uhr} Uhr` : '')
|
||||
+ (ende ? ` bis zum ${tagLang(ende)}` : ', bis du sie abstellst')
|
||||
+ '.';
|
||||
}
|
||||
|
||||
/* ---------- Die Liste der laufenden Wiederholungen ------------------------ */
|
||||
|
||||
async function serienLaden() {
|
||||
try {
|
||||
const a = await hole('/workspace/api/serien');
|
||||
if (!a.ok) { serien = []; return; }
|
||||
serien = (await a.json()).serien || [];
|
||||
} catch { serien = []; }
|
||||
}
|
||||
|
||||
function baueSerien() {
|
||||
const ziel = $('serien');
|
||||
ziel.replaceChildren();
|
||||
$('serien-block').hidden = !serien.length;
|
||||
if (!serien.length) return;
|
||||
|
||||
for (const s of serien) {
|
||||
const k = el('div', 'k-serie');
|
||||
k.dataset.aktiv = s.aktiv ? 'ja' : 'nein';
|
||||
const farbe = ARTFARBE[s.art] || ARTFARBE.termin;
|
||||
k.style.setProperty('--sfarbe', farbe);
|
||||
k.style.setProperty('--ton', farbe);
|
||||
|
||||
const kopf = el('div', 'k-serie__kopf');
|
||||
kopf.append(el('span', 'k-serie__titel', s.titel));
|
||||
kopf.append(el('span', 'k-serie__art', ARTNAME[s.art] || s.art));
|
||||
/* Der Zustand als Wort, nicht nur als Farbe -- "pausiert" muss man
|
||||
lesen können, ohne Farben unterscheiden zu müssen. */
|
||||
kopf.append(el('span', 'k-serie__zustand', s.aktiv ? 'läuft' : 'pausiert'));
|
||||
k.append(kopf);
|
||||
|
||||
/* Der Satz kommt vom Server. Bewusst nicht hier noch einmal
|
||||
gebaut: Wenn zwei Stellen denselben Satz formulieren, sagen sie
|
||||
irgendwann Verschiedenes -- und dann glaubt man der falschen. */
|
||||
k.append(el('p', 'k-serie__satz', s.satz));
|
||||
k.append(el('p', 'k-serie__naechster', s.naechster
|
||||
? 'Nächster Termin: ' + tagLang(s.naechster.slice(0, 10))
|
||||
+ ', ' + s.naechster.slice(11, 16) + ' Uhr'
|
||||
: s.aktiv ? 'Kein weiterer Termin – die Serie ist ausgelaufen.'
|
||||
: 'Pausiert – es kommt nichts nach.'));
|
||||
|
||||
if (s.darf_aendern) {
|
||||
const knoepfe = el('div', 'k-serie__knoepfe');
|
||||
knoepfe.append(tuKnopf(s.aktiv ? 'abstellen' : 'wieder starten', async () => {
|
||||
if (s.aktiv && !confirm(
|
||||
'Wiederholung „' + s.titel + '" abstellen?\n\n'
|
||||
+ 'Kommende Termine, die noch niemand angefasst hat, werden aus dem '
|
||||
+ 'Kalender genommen. Vergangene und bearbeitete Termine bleiben stehen.\n\n'
|
||||
+ 'Du kannst sie jederzeit wieder starten.')) return;
|
||||
const a = await hole('/workspace/api/serien/' + s.id, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ aktiv: s.aktiv ? 0 : 1 }),
|
||||
});
|
||||
if (!a.ok) { melde('Umschalten ging nicht.'); return; }
|
||||
await laden();
|
||||
}));
|
||||
knoepfe.append(tuKnopf('löschen', async () => {
|
||||
if (!confirm('Wiederholung „' + s.titel + '" ganz löschen?\n\n'
|
||||
+ 'Die Regel ist damit weg. Kommende, unangetastete Termine werden '
|
||||
+ 'entfernt; vergangene und bearbeitete bleiben als normale Termine '
|
||||
+ 'stehen.')) return;
|
||||
const a = await hole('/workspace/api/serien/' + s.id, { method: 'DELETE' });
|
||||
if (!a.ok) { melde('Löschen ging nicht.'); return; }
|
||||
await laden();
|
||||
}));
|
||||
k.append(knoepfe);
|
||||
}
|
||||
ziel.append(k);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Filter -------------------------------------------------------- */
|
||||
|
||||
function baueFilter() {
|
||||
@@ -570,7 +769,9 @@
|
||||
k.style.setProperty('--ton', e.farbe);
|
||||
|
||||
k.append(el('span', 'k-zeile__zeit', e.uhr ? e.uhr + ' Uhr' : '—'));
|
||||
k.append(el('span', 'k-zeile__titel', e.titel));
|
||||
const titel = el('span', 'k-zeile__titel', e.titel);
|
||||
if (e.serie_id) titel.append(wiederholZeichen());
|
||||
k.append(titel);
|
||||
|
||||
const rechts = el('div', 'k-zeile__knoepfe');
|
||||
rechts.append(el('span', 'k-zeile__art', ARTNAME[e.art] || e.art));
|
||||
@@ -591,7 +792,16 @@
|
||||
}));
|
||||
if (LEITUNG.has(ich.rolle) || e.erstellt_von === ich.id) {
|
||||
rechts.append(tuKnopf('löschen', async () => {
|
||||
if (!confirm('Termin „' + e.titel + '" löschen?')) return;
|
||||
/* Bei einem Serientermin muss unmissverständlich dastehen,
|
||||
dass NUR dieser eine Tag entfällt. Sonst löscht jemand ihn
|
||||
in der Annahme, die Wiederholung sei damit abgestellt --
|
||||
und wundert sich nächste Woche. */
|
||||
const frage = e.serie_id
|
||||
? 'Nur diesen einen Termin am ' + tagLang(e.tag) + ' streichen?\n\n'
|
||||
+ 'Die Wiederholung „' + e.titel + '" läuft weiter. Zum Abstellen '
|
||||
+ 'den Schalter unter „Läuft von allein" benutzen.'
|
||||
: 'Termin „' + e.titel + '" löschen?';
|
||||
if (!confirm(frage)) return;
|
||||
const a = await hole('/workspace/api/termine/' + e.id, { method: 'DELETE' });
|
||||
if (!a.ok) { melde('Löschen ging nicht.'); return; }
|
||||
await laden();
|
||||
@@ -765,8 +975,13 @@
|
||||
if (!a.ok) { melde('Kalender konnte nicht geladen werden.'); return; }
|
||||
const roh = await a.json();
|
||||
daten = { eintraege: vereinheitlichen(roh), heute: heuteText() };
|
||||
/* Die Wiederholungen NACH den Terminen holen: Der Serveraufruf
|
||||
oben legt fehlende Ausprägungen an, und erst danach stimmt der
|
||||
"nächste Termin" in der Liste darunter. */
|
||||
await serienLaden();
|
||||
baueSaison();
|
||||
baueMonatsanlaesse();
|
||||
baueSerien();
|
||||
zeichnen();
|
||||
/* Sprung vom Bericht -- erst NACH dem Zeichnen, vorher gibt es die
|
||||
Tagesbloecke nicht. Nur beim ersten Mal: Sonst reisst es die
|
||||
@@ -839,6 +1054,9 @@
|
||||
if (!tag) d.setHours(18, 0, 0, 0);
|
||||
$('f-beginn').value = new Date(d.getTime() - d.getTimezoneOffset() * 60000)
|
||||
.toISOString().slice(0, 16);
|
||||
/* Die Auswahl der Rhythmen hängt am Datum ("jeden Dienstag") und
|
||||
muss deshalb bei jedem Öffnen neu beschriftet werden. */
|
||||
taktAuswahlFuellen();
|
||||
umschalten(true);
|
||||
}
|
||||
|
||||
@@ -849,9 +1067,21 @@
|
||||
};
|
||||
$('neu-oeffnen').addEventListener('click', () => formularOeffnen(null));
|
||||
$('neu-abbrechen').addEventListener('click', () => {
|
||||
$('neu').reset(); $('neu-fehler').textContent = ''; umschalten(false);
|
||||
$('neu').reset();
|
||||
$('neu-fehler').textContent = '';
|
||||
/* reset() setzt die Auswahl auf ihren ersten Eintrag zurück, aber
|
||||
nicht den Vorschausatz und die Ende-Zeile -- die blieben sonst
|
||||
stehen und behaupteten beim nächsten Öffnen eine Wiederholung,
|
||||
die gar nicht gewählt ist. */
|
||||
$('f-takt').value = '';
|
||||
wdhVorschau();
|
||||
umschalten(false);
|
||||
});
|
||||
|
||||
$('f-beginn').addEventListener('change', taktAuswahlFuellen);
|
||||
$('f-takt').addEventListener('change', wdhVorschau);
|
||||
$('f-ende').addEventListener('change', wdhVorschau);
|
||||
|
||||
$('neu').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
$('neu-fehler').textContent = '';
|
||||
@@ -869,6 +1099,16 @@
|
||||
koerper.teilnehmer_id = wahl || null;
|
||||
koerper.creator_id = wahl || null;
|
||||
}
|
||||
/* Mit Rhythmus entsteht keine einzelne Zeile, sondern eine Regel --
|
||||
der Server legt daraus die Termine an, den ersten eingeschlossen.
|
||||
Deshalb wird hier nichts zusätzlich abgeschickt: Sonst stünde der
|
||||
erste Termin doppelt im Kalender. */
|
||||
if ($('f-takt').value) {
|
||||
koerper.wiederholung = {
|
||||
takt: $('f-takt').value,
|
||||
ende_tag: $('f-ende').value || null,
|
||||
};
|
||||
}
|
||||
knopf.disabled = true;
|
||||
try {
|
||||
const a = await hole('/workspace/api/termine', {
|
||||
@@ -880,7 +1120,10 @@
|
||||
$('neu-fehler').textContent = (await a.json().catch(() => ({}))).fehler || 'Ging nicht.';
|
||||
return;
|
||||
}
|
||||
$('neu').reset(); umschalten(false);
|
||||
$('neu').reset();
|
||||
$('f-takt').value = '';
|
||||
wdhVorschau();
|
||||
umschalten(false);
|
||||
/* Zum Tag des neuen Termins springen -- sonst legt man etwas an
|
||||
und sieht es nicht, weil es im nächsten Monat liegt. */
|
||||
const tag = koerper.beginn.slice(0, 10);
|
||||
|
||||
Reference in New Issue
Block a user