diff --git a/server/pruef-zeitraum.mjs b/server/pruef-zeitraum.mjs new file mode 100644 index 00000000..62252ab0 --- /dev/null +++ b/server/pruef-zeitraum.mjs @@ -0,0 +1,218 @@ +/* ===================================================================== + pruef-zeitraum.mjs — Ein Termin von–bis belegt jeden Tag dazwischen + + Filipe, 20.09.2026: „wenn ich termine eintrage mit von bis, will ich + dass die ganze zeit markiert ist und nicht nur der anfang." + + --------------------------------------------------------------------- + ZWEI FEHLER, NICHT EINER + + Der sichtbare: Die Kachel stand nur am Anfangstag. Wer im + Monatsraster auf den Mittwoch sah, sah nichts — obwohl die Aktion + von Montag bis Freitag lief. + + Der unsichtbare, und der ist der schlimmere: Der SERVER suchte + Einträge, deren ANFANG im sichtbaren Fenster liegt. Eine Aktion vom + 28.09. bis zum 05.10. kam im Oktober deshalb überhaupt nicht an — + nicht „nur am ersten Tag markiert", sondern gar nicht da. Wer im + Oktober plante, sah eine freie Woche, die belegt war. + + Deshalb wird hier BEIDES gemessen, und der zweite Fall zuerst. + + AUFRUF: node server/pruef-zeitraum.mjs + ===================================================================== */ + +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { portMussFreiSein } from "./helfer-port.mjs"; +import { notbremse } from "./helfer-notbremse.mjs"; + +const PORT = await portMussFreiSein(4437, "pruef-zeitraum"); +const ordner = mkdtempSync(join(tmpdir(), "ws-zeitraum-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +process.env.PORT = String(PORT); +process.env.SITE_ACCESS_SECRET = "lokaler-test"; +process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; +const express = (await import("express")).default; +const ec = express.response.cookie; +express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; +await import("./index.js"); +notbremse(230_000, "pruef-zeitraum"); +await new Promise((r) => setTimeout(r, 900)); +const BASIS = `http://127.0.0.1:${PORT}`; + +process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); }); +process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); }); + +let fehler = 0, geprueft = 0; +const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; + +console.log("\n=== Termine über mehrere Tage ===\n"); + +/* ---- Daten anlegen ------------------------------------------------ + RELATIV ZU HEUTE, nie mit einem festen Datum: Ein fest eingetragener + Termin wird vom Kalender eingeholt, und dann schlägt die Prüfung + fehl, ohne dass etwas kaputt ist (Hausregel, 06.09.2026). */ +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes } = await import("node:crypto"); +const d = new DatabaseSync(process.env.WORKSPACE_DB); +const salt = randomBytes(16).toString("hex"); +const hash = scryptSync("CODE-DOGI-0001", salt, 64, + { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); +d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt)" + + " VALUES (?,?,?,?,?,1,?)").run("Filipe", "admin", hash, salt, 32768, new Date().toISOString()); + +const tagPlus = (n) => { + const x = new Date(); + x.setHours(12, 0, 0, 0); + x.setDate(x.getDate() + n); + return x.toISOString().slice(0, 10); +}; +/* Eine Aktion über fünf Tage, die GESTERN angefangen hat — genau der + Fall, den der Server vorher verschluckt hat. */ +const LAUF_START = tagPlus(-1), LAUF_ENDE = tagPlus(3); +/* Und ein Punkttermin als Gegenprobe. */ +const EINZEL = tagPlus(5); + +const spalten = new Set(d.prepare("PRAGMA table_info(eintraege)").all().map((z) => z.name)); +ok(spalten.has("event_ende") && spalten.has("geplant"), + "die Tabelle kennt Anfang und Ende (event_ende, geplant)"); + +const einf = (titel, von, bis) => d.prepare( + "INSERT INTO eintraege (bereich, art, titel, text, status, dringlichkeit," + + " datum, geplant, event_ende, erstellt_von, erstellt)" + + " VALUES (?,?,?,?,?,?,?,?,?,?,?)") + .run("live", "idee", titel, "", "offen", "mittel", von, von, bis, 1, + new Date().toISOString()); +einf("Wochenaktion quer", LAUF_START, LAUF_ENDE); +einf("Nur ein Tag", EINZEL, null); +d.close(); + +const an = await fetch(BASIS + "/workspace/api/anmelden", { + method: "POST", headers: { "Content-Type": "application/json", Origin: BASIS }, + body: JSON.stringify({ rolle: "admin", code: "CODE-DOGI-0001" }), +}); +const keks = (an.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; "); +ok(an.ok && !!keks, "angemeldet"); + +/* ---- 1. DER SERVER: Überschneidung statt Anfang ------------------- */ +const hole = async (von, tage) => { + const a = await fetch(`${BASIS}/workspace/api/termine?von=${von}&tage=${tage}`, + { headers: { Cookie: keks } }); + return a.ok ? a.json() : null; +}; +const titel = (j) => (j?.brettsachen || []).map((b) => b.titel); + +/* Ein Fenster, das MITTEN im Zeitraum liegt — der Anfang ist davor. */ +const mitten = await hole(tagPlus(1), 2); +ok(titel(mitten).includes("Wochenaktion quer"), + "ein laufender Zeitraum kommt mit, auch wenn sein Anfang vor dem Fenster liegt"); +console.log(` Fenster ${tagPlus(1)} + 2 Tage, Aktion läuft ${LAUF_START}–${LAUF_ENDE}`); + +/* GEGENPROBE: Ein Fenster NACH dem Ende darf ihn nicht bringen. Ohne + diese Probe würde auch ein Server bestehen, der einfach alles + schickt — und dann stünde jeder je getippte Zettel im Kalender. */ +const danach = await hole(tagPlus(10), 5); +ok(!titel(danach).includes("Wochenaktion quer"), + "Gegenprobe: nach dem Ende kommt er nicht mehr — es wird nicht einfach alles geschickt"); +const davor = await hole(tagPlus(-30), 20); +ok(!titel(davor).includes("Wochenaktion quer"), + "Gegenprobe: und vor dem Anfang auch nicht"); + +/* Der Punkttermin muss sich genauso verhalten wie vorher. */ +const beimEinzel = await hole(EINZEL, 1); +ok(titel(beimEinzel).includes("Nur ein Tag"), + "der Punkttermin kommt weiterhin an seinem Tag"); + +/* ---- 2. DER BILDSCHIRM: jeder Tag ist markiert -------------------- */ +const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); +const browser = await pw.chromium.launch(); +try { + const ctx = await browser.newContext({ viewport: { width: 1400, height: 1000 } }); + const s = await ctx.newPage(); + await s.goto(BASIS + "/workspace/", { waitUntil: "networkidle" }); + await s.click('.rolle[data-rolle="admin"]'); + await s.fill("#code", "CODE-DOGI-0001"); + await s.click("button[type=submit]"); + await s.waitForURL("**/start.html", { timeout: 25000 }); + await s.goto(BASIS + "/workspace/kalender.html", { waitUntil: "networkidle" }); + await s.waitForTimeout(1800); + + /* Wie viele Tageszellen tragen die Aktion? */ + const lage = await s.evaluate(() => { + const zellen = [...document.querySelectorAll(".k-tag")]; + const treffer = []; + for (const z of zellen) { + for (const p of z.querySelectorAll(".k-pille")) { + if (/Wochenaktion quer/.test(p.textContent)) { + treffer.push({ + zahl: z.querySelector(".k-tag__zahl")?.textContent || "?", + spanne: p.dataset.spanne || null, + text: p.textContent.replace(/\s+/g, " ").trim(), + breit: Math.round(p.getBoundingClientRect().width), + }); + } + } + } + const einzel = [...document.querySelectorAll(".k-pille")] + .filter((p) => /Nur ein Tag/.test(p.textContent)); + return { zellen: zellen.length, treffer, + einzel: einzel.length, einzelSpanne: einzel[0]?.dataset.spanne ?? null }; + }); + + ok(lage.zellen > 0, `das Monatsraster steht (${lage.zellen} Tageszellen)`); + ok(lage.treffer.length === 5, + `die Aktion ist an allen fünf Tagen markiert (${lage.treffer.length})`); + console.log(" " + lage.treffer.map((t) => `${t.zahl}.[${t.spanne}]`).join(" ")); + + const pos = lage.treffer.map((t) => t.spanne); + ok(pos[0] === "start" && pos.at(-1) === "ende", + `Anfang und Ende sind als solche erkennbar (${pos[0]} … ${pos.at(-1)})`); + ok(pos.slice(1, -1).every((p) => p === "mitte"), + "dazwischen steht überall 'mitte' — die Form sagt, dass es weitergeht"); + + /* Auf dem ersten Tag steht die Uhrzeit-Spalte, danach die Lage. Eine + Uhrzeit am Mittwoch zu wiederholen behauptet, es ginge am Mittwoch + wieder los. */ + ok(/Tag 2\/5/.test(lage.treffer[1]?.text || ""), + `der zweite Tag sagt, der wievielte er ist ("${lage.treffer[1]?.text.slice(0, 34)}")`); + ok(!/Tag \d+\/\d+/.test(lage.treffer[0]?.text || ""), + "am ersten Tag steht das NICHT — dort gehört die Startzeit hin"); + + /* GEGENPROBE AM BILDSCHIRM: Der Punkttermin darf sich nicht + angesteckt haben. Ohne sie bestünde auch eine Fassung, die jede + Kachel über den halben Monat zieht. */ + ok(lage.einzel === 1, + `Gegenprobe: der Punkttermin steht an genau einem Tag (${lage.einzel})`); + ok(lage.einzelSpanne === null, + "Gegenprobe: und trägt keine Spannen-Kennzeichnung"); + + /* ---- 3. Der Tagesdialog widerspricht dem Raster nicht ----------- */ + /* Vorher filterte er selbst auf den Anfangstag: Im Raster war der + Mittwoch markiert, tippte man ihn an, stand da „An diesem Tag + steht nichts." */ + const mitteTag = lage.treffer[2]?.zahl; + await s.evaluate((zahl) => { + for (const z of document.querySelectorAll(".k-tag")) { + if (z.querySelector(".k-tag__zahl")?.textContent === zahl) { z.click(); return; } + } + }, mitteTag); + await s.waitForTimeout(700); + const dialog = await s.evaluate(() => { + const dl = document.querySelector("#tagdialog[open]"); + return dl ? dl.textContent.replace(/\s+/g, " ").trim() : null; + }); + ok(!!dialog && /Wochenaktion quer/.test(dialog), + `der Tagesdialog am ${mitteTag}. zeigt die Aktion auch`); + ok(!/An diesem Tag steht nichts/.test(dialog || ""), + " und sagt nicht mehr 'An diesem Tag steht nichts'"); + + await ctx.close(); +} finally { + await browser.close(); +} + +console.log(`\n${fehler ? fehler + " FEHLER" : "Alles in Ordnung"} — ${geprueft} Prüfungen\n`); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } +process.exit(fehler ? 1 : 0); diff --git a/server/workspace-kalender.js b/server/workspace-kalender.js index 5beb954c..1916d982 100644 --- a/server/workspace-kalender.js +++ b/server/workspace-kalender.js @@ -36,7 +36,7 @@ import { sichtbarEintrag, BEREICHE } from "./workspace-bereiche.js"; laufen auseinander, und dann zeigt die Karte etwas anderes als der Kalender. */ import { - TERMIN_TAG_SQL, TERMIN_BEDINGUNG_SQL, TERMIN_GRUND_SQL, OHNE_ZUSTAENDE, + TERMIN_TAG_SQL, TERMIN_ENDE_SQL, TERMIN_BEDINGUNG_SQL, TERMIN_GRUND_SQL, OHNE_ZUSTAENDE, } from "./workspace-termin-regel.js"; export const kalenderRouter = express.Router(); @@ -463,10 +463,17 @@ kalenderRouter.get("/workspace/api/termine", (req, res) => { WHERE ${eintragRegel.wo} AND e.status NOT IN (${OHNE_ZUSTAENDE.map(() => "?").join(", ")}) AND ${TERMIN_BEDINGUNG_SQL} - AND ${TERMIN_TAG_SQL} >= ? + /* UEBERSCHNEIDUNG, NICHT ANFANG (20.09.2026). + Vorher stand hier zweimal TERMIN_TAG_SQL -- also: „faengt + im sichtbaren Fenster an". Eine Aktion vom 28.09. bis zum + 05.10. fiel damit im Oktober komplett heraus. Richtig ist + die Frage, ob sich Zeitraum und Fenster BERUEHREN: + faengt vor dem Fensterende an UND hoert nach dem + Fensteranfang auf. */ AND ${TERMIN_TAG_SQL} <= ? + AND ${TERMIN_ENDE_SQL} >= ? ORDER BY wann, e.uhrzeit, e.id`) - .all(...eintragRegel.werte, ...OHNE_ZUSTAENDE, heuteLokal(), von, bis) : []; + .all(...eintragRegel.werte, ...OHNE_ZUSTAENDE, heuteLokal(), bis, von) : []; /* DER NAME DES BRETTS KOMMT MIT -- und zwar aus dem Katalog, nicht als abgeschriebene Liste im Browser. Ein Brett, das morgen diff --git a/server/workspace-termin-regel.js b/server/workspace-termin-regel.js index 96c36067..a1097d24 100644 --- a/server/workspace-termin-regel.js +++ b/server/workspace-termin-regel.js @@ -52,6 +52,26 @@ * den 24., auch wenn der Zettel am 3. entstanden ist. */ export const TERMIN_TAG_SQL = "COALESCE(e.geplant, e.datum)"; +/** Der LETZTE Tag, an dem ein Eintrag noch laeuft -- als SQL-Ausdruck. + * + * Gebraucht seit dem 20.09.2026. Filipe: „wenn ich termine eintrage + * mit von bis, will ich dass die ganze zeit markiert ist und nicht + * nur der anfang." + * + * DER FEHLER SASS TIEFER ALS IN DER ANZEIGE. Der Kalender suchte + * Eintraege, deren ANFANG im sichtbaren Fenster liegt. Eine Aktion + * vom 28.09. bis zum 05.10. kam damit im Oktober ueberhaupt nicht an + * -- nicht „nur am ersten Tag markiert", sondern gar nicht da. Wer im + * Oktober plante, sah eine freie Woche, die belegt war. + * + * `MAX` und nicht `COALESCE(e.event_ende, ...)`: Ein Ende, das VOR + * dem Anfang liegt (vertippt, Anfang spaeter verschoben), wuerde + * sonst den Eintrag aus seinem eigenen Zeitraum werfen. Der leere + * Text sortiert vor jedem Datum -- ohne Ende gewinnt damit immer der + * Anfang, und das ist genau richtig. */ +export const TERMIN_ENDE_SQL = + "MAX(COALESCE(e.event_ende, ''), COALESCE(e.geplant, e.datum))"; + /** Die Bedingung als SQL. Erwartet GENAU EINEN Platzhalter: den * heutigen Tag in Ortszeit (`heuteLokal()`). * diff --git a/workspace/assets/css/kalender.css b/workspace/assets/css/kalender.css index 6cfaaec8..56ed0629 100644 --- a/workspace/assets/css/kalender.css +++ b/workspace/assets/css/kalender.css @@ -355,6 +355,75 @@ Kontrastmodus nicht zu erkennen. */ .k-pille.erledigt { opacity: .5; text-decoration: line-through; } +/* ===================================================================== + EIN TERMIN, DER MEHRERE TAGE LAEUFT (20.09.2026) + + Filipe: „wenn ich termine eintrage mit von bis, will ich dass die + ganze zeit markiert ist und nicht nur der anfang." + + Die Kachel steht jetzt in jeder Tageszelle dazwischen. Damit man + sieht, dass es EIN Termin ist und nicht vier gleichnamige, wird sie + an den Seiten angeschnitten, an denen es weitergeht: + + Mo [ 20:00 Aktion ━ + Di ━ Tag 2/4 ━ + Mi ━ Tag 3/4 ━ + Do ━ Tag 4/4 ] + + DIE FORM MACHT DIE AUSSAGE, nicht die Farbe -- an einer eckigen + Kante sieht man „geht weiter" auch dann, wenn man Farben schlecht + unterscheidet oder der Kontrastmodus alles einfaerbt. + + WARUM KEIN ECHTER BALKEN UEBER DIE ZELLEN: Ein Element, das sich + ueber mehrere Rasterzellen legt, muesste wissen, wo die Woche + umbricht, wie hoch die Zeile gerade ist und was darunter noch steht. + Das ist bei jeder Fensterbreite eine andere Rechnung -- und eine + Rechnung, die irgendwann danebenliegt, schiebt Kacheln uebereinander. + Die angeschnittene Kante kostet nichts und kann nicht verrutschen. */ + +/* Laeuft gestern schon: links glatt abgeschnitten, kein Randstrich. */ +.k-pille[data-spanne="mitte"], +.k-pille[data-spanne="ende"] { + border-left: 0; + border-top-left-radius: 0; border-bottom-left-radius: 0; + /* Der Strich am linken Rand wird zum FORTSETZUNGSZEICHEN: ein + schmaler, waagerechter Balken in der Farbe, auf halber Hoehe. */ + padding-left: 12px; + background-image: linear-gradient(90deg, + var(--pfarbe) 0 4px, transparent 4px); + background-repeat: no-repeat; + background-position: 0 50%; + background-size: 100% 2px; +} +/* Geht morgen weiter: rechts glatt abgeschnitten. */ +.k-pille[data-spanne="start"], +.k-pille[data-spanne="mitte"] { + border-top-right-radius: 0; border-bottom-right-radius: 0; + /* Eine Kante, die zeigt, dass hier nichts endet. */ + box-shadow: inset -3px 0 0 -1px color-mix(in srgb, var(--pfarbe) 70%, transparent); +} +/* Der ganze Zeitraum liegt etwas satter als ein Punkttermin -- er + BELEGT die Tage, und das soll man beim Ueberfliegen sehen. */ +.k-pille[data-spanne] { + background-color: color-mix(in srgb, var(--pfarbe) 22%, transparent); +} +.k-pille[data-spanne]:hover { + background-color: color-mix(in srgb, var(--pfarbe) 32%, transparent); +} +/* „Tag 2/4" ist eine Angabe zur Lage, keine Uhrzeit -- deshalb ruhiger + als die fette Startzeit daneben. */ +.k-pille__lauf { font-weight: 550; opacity: .82; } + +@media (forced-colors: active) { + /* Im Kontrastmodus verschwinden Farbflaechen und Schatten. Dann muss + die Kante als echter Rahmen dastehen, sonst sieht die Kachel am + Mittwoch genauso aus wie ein eigener Termin. */ + .k-pille[data-spanne="start"], + .k-pille[data-spanne="mitte"] { border-right: 2px dashed CanvasText; } + .k-pille[data-spanne="mitte"], + .k-pille[data-spanne="ende"] { border-left: 2px dashed CanvasText; } +} + /* ---------- Wochenansicht ----------------------------------------------- */ .k-woche { display: grid; gap: 1px; diff --git a/workspace/assets/js/kalender.js b/workspace/assets/js/kalender.js index 2a04912e..57648405 100644 --- a/workspace/assets/js/kalender.js +++ b/workspace/assets/js/kalender.js @@ -601,11 +601,93 @@ statt verschluckt. */ const sichtbare = () => (daten ? daten.eintraege.filter((e) => zeigen[e.art] !== false) : []); + /* ===================================================================== + EIN TERMIN VON--BIS STEHT AN JEDEM TAG DAZWISCHEN (20.09.2026) + + Filipe: „wenn ich termine eintrage mit von bis, will ich dass die + ganze zeit markiert ist und nicht nur der anfang." + + BISHER stand er nur am Anfangstag. Wer im Monatsraster auf den + Mittwoch sah, sah nichts -- obwohl die Aktion von Montag bis + Freitag lief. Genau in der Ansicht, in der man plant, war der + belegte Zeitraum unsichtbar, und man legte etwas mitten hinein. + + WARUM GENAU HIER: `nachTag()` ist die EINZIGE Stelle, an der + Eintraege auf Tage verteilt werden -- Monat, Woche, Liste und + Zeitstrahl holen sich alle `karte.get(tag)`. Vier Ansichten + einzeln nachzuruesten waere vier Stellen, an denen die naechste + Ansicht vergessen wird. + + DAS ENDE WIRD ABGELEITET, NICHT GEPFLEGT: + * `bis` -- Brettkarten mit event_ende + * Uhr + Dauer -- ein Live von 22:00 ueber 240 Minuten endet um + 02:00 am naechsten Tag. Das stand bisher + NIRGENDS, obwohl die Zahlen da waren. + Eine Liste „diese Arten haben ein Ende" waere die Liste, die beim + naechsten Termintyp veraltet. + + DIE KOPIEN AENDERN `tag` NICHT. Wer auf eine Kachel am Mittwoch + tippt, will den Termin bearbeiten -- und der faengt am Montag an. + Wuerde die Kopie `tag` auf den Mittwoch setzen, verschoebe ein + Speichern den Termin auf den Tag, an dem man zufaellig + hingetippt hat. Der Anzeigetag steht deshalb getrennt daneben. */ + + /** Der letzte Tag, an dem dieser Eintrag noch laeuft. */ + function endTagVon(e) { + if (e.bis) return String(e.bis).slice(0, 10); + if (e.uhr && Number(e.dauer_min) > 0) { + const [st, mi] = e.uhr.split(':').map(Number); + const start = ausText(e.tag); + if (Number.isNaN(st) || Number.isNaN(start.getTime())) return e.tag; + /* AB MITTERNACHT RECHNEN, NICHT AB `ausText`. `ausText` legt den + Tag bewusst auf 12:00 UTC (damit Zeitzonen ihn nicht um einen + Tag verschieben) -- wer darauf die Uhrzeit ADDIERT, rechnet + zwoelf Stunden zu viel. Erster Anlauf ergab damit fuer + „13:00, 60 Minuten" den FOLGETAG. */ + const mitternacht = Date.UTC( + start.getUTCFullYear(), start.getUTCMonth(), start.getUTCDate()); + /* Minus eine Millisekunde: Ein Stream von 22:00 ueber genau zwei + Stunden endet um 00:00 -- er laeuft am naechsten Tag NICHT + mehr, auch wenn die Zahl dort steht. */ + const endeMs = mitternacht + + ((st * 60 + mi + Number(e.dauer_min)) * 60000) - 1; + return alsText(new Date(Math.max(endeMs, mitternacht))); + } + return e.tag; + } + + /* Eine Obergrenze, die nicht schoen ist, aber ehrlich: Ein Eintrag + mit kaputtem Enddatum (Jahr 9999) wuerde sonst Millionen Kopien + bauen und den Browser anhalten. 400 Tage sind mehr als jede + Ansicht je zeigt -- die Grenze ist also nie im Weg und faengt nur + den Unfall ab. */ + const SPANNE_MAX = 400; + function nachTag() { const karte = new Map(); + const lege = (tag, e) => { + if (!karte.has(tag)) karte.set(tag, []); + karte.get(tag).push(e); + }; for (const e of sichtbare()) { - if (!karte.has(e.tag)) karte.set(e.tag, []); - karte.get(e.tag).push(e); + const ende = endTagVon(e); + if (!ende || ende <= e.tag) { lege(e.tag, e); continue; } + + const tage = Math.min(SPANNE_MAX, + Math.round((ausText(ende) - ausText(e.tag)) / 86400000) + 1); + let d = ausText(e.tag); + for (let i = 0; i < tage; i++) { + lege(alsText(d), { + ...e, + /* Der Tag, an dem die Kachel steht -- NICHT `tag`. */ + spann_tag: alsText(d), + spann_nr: i + 1, + spann_tage: tage, + spann_bis: ende, + spann_pos: i === 0 ? 'start' : (i === tage - 1 ? 'ende' : 'mitte'), + }); + d = plus(d, 1); + } } return karte; } @@ -629,8 +711,38 @@ if (e.ziel) k.href = e.ziel; k.style.setProperty('--pfarbe', e.farbe); 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)); + + (e.serie_id ? ' — wiederholt sich' : '') + /* Der ganze Zeitraum, nicht nur „Tag 2/4": Wer am Mittwoch + draufzeigt, will wissen, WANN es anfing und aufhoert -- sonst + muss er selbst zurueckblaettern und zaehlen. */ + + (e.spann_tage > 1 + ? ` — läuft vom ${tagLang(e.tag)} bis ${tagLang(e.spann_bis)}` + + ` (Tag ${e.spann_nr} von ${e.spann_tage})` + : ''); + /* ---- LAEUFT UEBER MEHRERE TAGE (20.09.2026) -------------------- + + Drei Dinge unterscheiden die Kachel eines Zeitraums von der eines + Punkttermins, und alle drei sind noetig: + + 1. `data-spanne` -- daraus macht das Stilblatt einen + durchgehenden Balken: links angeschnitten, wo er gestern + schon lief, rechts angeschnitten, wo er morgen weiterlaeuft. + Man sieht an der FORM, dass es weitergeht, ohne ein Wort. + 2. „Tag 2/4" statt der Uhrzeit auf den Folgetagen. Die Uhrzeit + gehoert zum Anfang; sie am Mittwoch zu wiederholen behauptet, + es ginge am Mittwoch um 20:00 wieder los. + 3. Der ganze Zeitraum im Titel -- fuer Maus und Vorleseprogramm. + + AUF DEM ANFANGSTAG BLEIBT ALLES WIE VORHER. Wer den Termin + eintraegt, sieht dort die Uhrzeit, die er eingetragen hat. */ + if (e.spann_tage > 1) { + k.dataset.spanne = e.spann_pos; + k.style.setProperty('--spann-tage', String(e.spann_tage)); + } + if (e.spann_tage > 1 && e.spann_pos !== 'start') { + k.append(el('span', 'k-pille__zeit k-pille__lauf', + `Tag ${e.spann_nr}/${e.spann_tage}`)); + } else 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 @@ -1355,7 +1467,16 @@ const liste = $('tagdialog-liste'); liste.replaceChildren(); - const eintraege = sichtbare().filter((e) => e.tag === tag); + /* UEBER `nachTag()`, NICHT UEBER `e.tag` (20.09.2026). + + Vorher stand hier ein eigener Filter auf den Anfangstag. Folge: + Im Monatsraster war die Aktion am Mittwoch markiert -- tippte man + die Zelle an, sagte der Tagesdialog „An diesem Tag steht + nichts." Zwei Antworten auf dieselbe Frage, und die zweite + widerspricht dem, was man gerade gesehen hat. + + `nachTag()` ist die eine Stelle, die das entscheidet. */ + const eintraege = nachTag().get(tag) || []; if (!eintraege.length) { liste.append(el('p', 'leise', 'An diesem Tag steht nichts.')); } else {