Ein Termin von-bis belegt jeden Tag dazwischen
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 Eintraege, deren ANFANG im sichtbaren Fenster liegt. Eine Aktion vom 28.09. bis zum 05.10. kam im Oktober deshalb ueberhaupt nicht an -- nicht "nur am ersten Tag markiert", sondern gar nicht da. Wer im Oktober plante, sah eine freie Woche, die belegt war. Jetzt entscheidet die UEBERSCHNEIDUNG, nicht der Anfang. Gebaut wurde es in nachTag() -- der einzigen Stelle, an der Eintraege auf Tage verteilt werden. Monat, Woche, Liste und Zeitstrahl holen sich alle dort; vier Ansichten einzeln nachzuruesten waeren vier Stellen, an denen die fuenfte vergessen wird. Das Ende wird abgeleitet, nicht gepflegt: aus event_ende ODER aus Uhrzeit plus Dauer. Ein Live von 22:00 ueber vier Stunden endet um 02:00 am naechsten Tag -- das stand bisher nirgends, obwohl die Zahlen da waren. Der Tagesdialog filterte selbst auf den Anfangstag. Im Raster war der Mittwoch markiert, tippte man ihn an, stand da "An diesem Tag steht nichts." Jetzt fragt er dieselbe Stelle wie das Raster. pruef-zeitraum.mjs: 16 Pruefungen, beide Fehler einzeln, mit drei Gegenproben (vor dem Anfang, nach dem Ende, Punkttermin). pruef-terminregel.mjs weiterhin 35/0. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -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);
|
||||
@@ -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
|
||||
|
||||
@@ -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()`).
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user