diff --git a/CHANGELOG.md b/CHANGELOG.md
index dc79eda..cc9e1df 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,33 @@
All notable changes to this project are documented in this file.
+## [1.129.0] - 2026-09-14
+
+### Added
+- **Neue "Überfällig"-Übersicht** für Aufgaben, die in der Vergangenheit geplant und
+ nicht erledigt wurden. Bislang wandern nur Aufgaben mit aktiviertem Rolling-Schalter
+ (oder bei global aktiviertem Auto-Rolling) automatisch auf heute — alles andere blieb
+ für immer auf seinem ursprünglichen Datum stehen und verschwand aus dem Blick, sobald
+ man in der Wochenansicht weitergeklickt hat.
+ - Neuer Menüpunkt "Überfällig" mit Anzahl-Badge, direkt neben "Wiederkehrend" in allen
+ bestehenden Menü-Varianten (linke/obere Randleiste, mobile Leiste, Kopfzeile).
+ - Dialog gruppiert nach dem ursprünglich geplanten Tag (älteste zuerst), pro Aufgabe
+ Checkbox zum Erledigen und ein Knopf zum Verschieben auf heute, dazu "Alle auf
+ heute verschieben".
+ - Reine Erkennungs-/Gruppierungslogik in `src/lib/weekly/overdue.ts` (10 Unit-Tests),
+ Darstellung in `src/components/OverdueTasksManager.tsx` — nutzt `LinkifiedText`
+ (v1.128.0), Titel mit URLs bleiben also auch hier anklickbar.
+ - Verschieben/Erledigen läuft über die bestehenden `moveTaskToSlot`/`toggleTask`-Pfade,
+ keine neue API, keine neue Persistenzlogik.
+
+Verifiziert nicht nur mit Unit-Tests: lokale Instanz mit Wegwerf-Postgres (Docker)
+aufgesetzt, drei überfällige Aufgaben geseedet (eine davon mit URL im Titel), per
+Playwright eingeloggt und die Übersicht geöffnet — Badge, Gruppierung und Link-Rendering
+per Screenshot geprüft. Danach real geklickt: eine Aufgabe per Checkbox erledigt, eine
+per Knopf auf heute verschoben, die restlichen zwei per Sammelknopf — alle drei Änderungen
+per Datenbankabfrage bestätigt. Zusätzlich: Typecheck, Lint (0 Fehler), 118 Unit-Tests
+(10 neu), Produktions-Build.
+
## [1.128.0] - 2026-09-14
### Fixed
diff --git a/package.json b/package.json
index fcf187b..5750e7d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "my-weekly-todo-list",
- "version": "1.128.0",
+ "version": "1.129.0",
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
"main": "index.js",
"scripts": {
diff --git a/src/components/OverdueTasksManager.tsx b/src/components/OverdueTasksManager.tsx
new file mode 100644
index 0000000..bae1353
--- /dev/null
+++ b/src/components/OverdueTasksManager.tsx
@@ -0,0 +1,139 @@
+import React, { useMemo } from "react";
+import { AlertCircle, X, ArrowRight, ArrowRightCircle } from "lucide-react";
+import type { Task } from "@/lib/weekly/types";
+import { getOverdueTasks, groupOverdueByDate } from "@/lib/weekly/overdue";
+import { LinkifiedText } from "./weekly/LinkifiedText";
+
+interface OverdueTasksManagerProps {
+ isOpen: boolean;
+ onClose: () => void;
+ tasks: Task[];
+ language?: string;
+ onToggleComplete: (taskId: string) => void;
+ onMoveToToday: (task: Task) => void;
+ onMoveAllToToday: (tasks: Task[]) => void;
+}
+
+/**
+ * Aufgaben, die in der Vergangenheit geplant und nicht erledigt wurden.
+ *
+ * Nur Aufgaben mit "isRolling" oder bei global aktiviertem "autoRolling" wandern
+ * automatisch auf heute (siehe rollOverdueTasks in WeeklyView.tsx) -- alles andere
+ * bleibt sonst für immer auf seinem ursprünglichen Datum stehen und verschwindet aus
+ * dem Blick, sobald man in der Wochenansicht weitergeklickt hat. Dieser Dialog macht
+ * genau diese Aufgaben sichtbar, unabhängig vom Rolling-Status, gruppiert nach dem
+ * Datum, an dem sie eigentlich fällig waren.
+ *
+ * Die Erkennungs-/Gruppierungslogik selbst liegt als reine Funktion in
+ * src/lib/weekly/overdue.ts -- diese Datei enthält bewusst nur JSX/Rendering.
+ */
+
+export default function OverdueTasksManager({
+ isOpen,
+ onClose,
+ tasks,
+ language,
+ onToggleComplete,
+ onMoveToToday,
+ onMoveAllToToday,
+}: OverdueTasksManagerProps) {
+ const de = language === "de";
+ const overdue = useMemo(() => getOverdueTasks(tasks), [tasks]);
+ const groups = useMemo(() => groupOverdueByDate(overdue), [overdue]);
+
+ if (!isOpen) return null;
+
+ const dateLabel = (dateStr: string) => {
+ const date = new Date(dateStr + "T00:00:00");
+ return date.toLocaleDateString(de ? "de-DE" : "en-US", {
+ weekday: "short",
+ day: "numeric",
+ month: "short",
+ });
+ };
+
+ return (
+
+
e.stopPropagation()} style={{ maxHeight: "80vh" }}>
+
+
+
+
+ {de ? "Überfällig" : "Overdue"}
+ {overdue.length > 0 && (
+ ({overdue.length})
+ )}
+
+
+
+
+
+
+
+
+ {groups.length === 0 ? (
+
+
+
+ {de ? "Nichts Überfälliges — alles erledigt oder rechtzeitig verschoben." : "Nothing overdue — all caught up."}
+
+
+ ) : (
+
+ {groups.map(({ dateStr, tasks: dayTasks }) => (
+
+
+ {dateLabel(dateStr)}
+
+
+ {dayTasks.map((task) => (
+
+
onToggleComplete(task.id)}
+ className="w-4 h-4 flex-shrink-0 cursor-pointer"
+ aria-label={de ? `"${task.title}" erledigt` : `Mark "${task.title}" complete`}
+ />
+
+
+
+
onMoveToToday(task)}
+ className="flex items-center gap-1.5 px-3 py-1.5 bg-blue-50 hover:bg-blue-100 dark:bg-blue-900/10 dark:hover:bg-blue-900/20 text-blue-600 dark:text-blue-400 rounded-lg text-xs font-bold transition-colors flex-shrink-0"
+ title={de ? "Auf heute verschieben" : "Move to today"}
+ >
+
+ {de ? "Heute" : "Today"}
+
+
+ ))}
+
+
+ ))}
+
+ )}
+
+
+ {overdue.length > 0 && (
+
+
onMoveAllToToday(overdue)}
+ className="flex items-center gap-2 px-4 py-2 bg-teal-500 hover:bg-teal-600 text-white rounded-lg text-sm font-bold transition-colors"
+ >
+
+ {de ? "Alle auf heute verschieben" : "Move all to today"}
+
+
+ )}
+
+
+ );
+}
diff --git a/src/components/WeeklyView.tsx b/src/components/WeeklyView.tsx
index 7e4a3c6..738462e 100644
--- a/src/components/WeeklyView.tsx
+++ b/src/components/WeeklyView.tsx
@@ -116,6 +116,8 @@ import UserMenu from "./UserMenu";
import SearchModal from "./SearchModal";
import SimpleDatePicker from "./SimpleDatePicker";
import RecurringTasksManager from "./RecurringTasksManager";
+import OverdueTasksManager from "./OverdueTasksManager";
+import { getOverdueTasks } from "@/lib/weekly/overdue";
import WeekPrintModal from "./WeekPrintModal";
import { ProjectIcon } from "./weekly/ProjectIcon";
import { ProjectsSidebar } from "./weekly/ProjectsSidebar";
@@ -936,6 +938,8 @@ export default function WeeklyView() {
// New UI State
const [isSearchOpen, setIsSearchOpen] = useState(false);
const [isRecurringTasksOpen, setIsRecurringTasksOpen] = useState(false);
+ const [isOverdueTasksOpen, setIsOverdueTasksOpen] = useState(false);
+ const overdueCount = useMemo(() => getOverdueTasks(tasks).length, [tasks]);
const [showDatePicker, setShowDatePicker] = useState(false);
const datePickerBtnRef = useRef(null);
const [showNavDatePicker, setShowNavDatePicker] = useState(false);
@@ -5494,6 +5498,7 @@ export default function WeeklyView() {
{ const now = new Date(); setCalendarEventModal({ isOpen: true, event: undefined, initialDate: now, initialStartTime: `${String(now.getHours()).padStart(2, "0")}:00` }); setShowQuickSettings(false); }} style={qsActionStyle(darkMode)}> {profile.language === "de" ? "Kalendereintrag" : "Calendar Event"}
{ setShowProjectsSidebar(true); setShowQuickSettings(false); }} style={qsActionStyle(darkMode)}> {profile.language === "de" ? "Projekte" : "Projects"}
{ setIsRecurringTasksOpen(true); setShowQuickSettings(false); }} style={qsActionStyle(darkMode)}> {profile.language === "de" ? "Wiederkehrend" : "Recurring Tasks"}
+ { setIsOverdueTasksOpen(true); setShowQuickSettings(false); }} style={qsActionStyle(darkMode)}> {profile.language === "de" ? "Überfällig" : "Overdue"}{overdueCount > 0 ? ` (${overdueCount})` : ""}
{ const nv = !profile.showNextTask; setShowNextTask(nv); saveSetting("showNextTask", nv); }} style={qsActionStyle(darkMode)}>{profile.showNextTask ? : }{profile.showNextTask ? (profile.language === "de" ? "Nächste Aufgabe" : "Next Task") : (profile.language === "de" ? "Ziel" : "Goal")}
{ setShowFocusMode(true); setShowQuickSettings(false); }} style={qsActionStyle(darkMode)}> {profile.language === "de" ? "Fokus" : "Focus"}
@@ -5558,6 +5563,7 @@ export default function WeeklyView() {
{ const now = new Date(); setCalendarEventModal({ isOpen: true, event: undefined, initialDate: now, initialStartTime: `${String(now.getHours()).padStart(2, "0")}:00` }); setShowQuickSettings(false); }} style={qsActionStyle(darkMode)}> {profile.language === "de" ? "Kalendereintrag" : "Calendar Event"}
{ setShowProjectsSidebar(true); setShowQuickSettings(false); }} style={qsActionStyle(darkMode)}> {profile.language === "de" ? "Projekte" : "Projects"}
{ setIsRecurringTasksOpen(true); setShowQuickSettings(false); }} style={qsActionStyle(darkMode)}> {profile.language === "de" ? "Wiederkehrend" : "Recurring Tasks"}
+ { setIsOverdueTasksOpen(true); setShowQuickSettings(false); }} style={qsActionStyle(darkMode)}> {profile.language === "de" ? "Überfällig" : "Overdue"}{overdueCount > 0 ? ` (${overdueCount})` : ""}
{ const nv = !profile.showNextTask; setShowNextTask(nv); saveSetting("showNextTask", nv); }} style={qsActionStyle(darkMode)}>{profile.showNextTask ? : }{profile.showNextTask ? (profile.language === "de" ? "Nächste Aufgabe" : "Next Task") : (profile.language === "de" ? "Ziel" : "Goal")}
{ setShowFocusMode(true); setShowQuickSettings(false); }} style={qsActionStyle(darkMode)}> {profile.language === "de" ? "Fokus" : "Focus"}
@@ -5623,6 +5629,7 @@ export default function WeeklyView() {
{ const now = new Date(); setCalendarEventModal({ isOpen: true, event: undefined, initialDate: now, initialStartTime: `${String(now.getHours()).padStart(2, "0")}:00` }); }} style={qsActionStyle(darkMode)}> {profile.language === "de" ? "Kalendereintrag" : "Calendar Event"}
setShowProjectsSidebar(true)} style={qsActionStyle(darkMode)}> {profile.language === "de" ? "Projekte" : "Projects"}
setIsRecurringTasksOpen(true)} style={qsActionStyle(darkMode)}> {profile.language === "de" ? "Wiederkehrend" : "Recurring Tasks"}
+ setIsOverdueTasksOpen(true)} style={qsActionStyle(darkMode)}> {profile.language === "de" ? "Überfällig" : "Overdue"}{overdueCount > 0 ? ` (${overdueCount})` : ""}
{ const nv = !profile.showNextTask; setShowNextTask(nv); saveSetting("showNextTask", nv); }} style={qsActionStyle(darkMode)}>{profile.showNextTask ? : }{profile.showNextTask ? (profile.language === "de" ? "Nächste Aufgabe" : "Next Task") : (profile.language === "de" ? "Ziel" : "Goal")}
setShowFocusMode(true)} style={qsActionStyle(darkMode)}> {profile.language === "de" ? "Fokus" : "Focus"}
@@ -5683,6 +5690,7 @@ export default function WeeklyView() {
{ const now = new Date(); setCalendarEventModal({ isOpen: true, event: undefined, initialDate: now, initialStartTime: `${String(now.getHours()).padStart(2, "0")}:00` }); }} style={{ ...railIconBtnStyle }} title={profile.language === "de" ? "Kalendereintrag" : "Calendar Event"}>
setShowProjectsSidebar(true)} style={{ ...railIconBtnStyle }} title={profile.language === "de" ? "Projekte" : "Projects"}>
setIsRecurringTasksOpen(true)} style={{ ...railIconBtnStyle }} title={profile.language === "de" ? "Wiederkehrend" : "Recurring"}>
+ setIsOverdueTasksOpen(true)} style={{ ...railIconBtnStyle }} title={`${profile.language === "de" ? "Überfällig" : "Overdue"}${overdueCount > 0 ? ` (${overdueCount})` : ""}`}>
{ const nv = !profile.showNextTask; setShowNextTask(nv); saveSetting("showNextTask", nv); }} style={{ ...railIconBtnStyle }} title={profile.language === "de" ? "Ziel" : "Goal"}>{profile.showNextTask ? : }
setShowFocusMode(true)} style={{ ...railIconBtnStyle }} title={profile.language === "de" ? "Fokus" : "Focus"}>
{railSep}
@@ -5922,6 +5930,7 @@ export default function WeeklyView() {
{ const now = new Date(); setCalendarEventModal({ isOpen: true, event: undefined, initialDate: now, initialStartTime: `${String(now.getHours()).padStart(2, "0")}:00` }); setShowMobileRail(false); setFlyoutSection(null); }} style={{ ...railIconBtnStyle }} title={profile.language === "de" ? "Kalendereintrag" : "Calendar Event"}>
{ setShowProjectsSidebar(true); setShowMobileRail(false); setFlyoutSection(null); }} style={{ ...railIconBtnStyle }} title={profile.language === "de" ? "Projekte" : "Projects"}>
{ setIsRecurringTasksOpen(true); setShowMobileRail(false); setFlyoutSection(null); }} style={{ ...railIconBtnStyle }} title={profile.language === "de" ? "Wiederkehrend" : "Recurring"}>
+ { setIsOverdueTasksOpen(true); setShowMobileRail(false); setFlyoutSection(null); }} style={{ ...railIconBtnStyle }} title={`${profile.language === "de" ? "Überfällig" : "Overdue"}${overdueCount > 0 ? ` (${overdueCount})` : ""}`}>
{ const nv = !profile.showNextTask; setShowNextTask(nv); saveSetting("showNextTask", nv); }} style={{ ...railIconBtnStyle }} title={profile.language === "de" ? "Ziel" : "Goal"}>{profile.showNextTask ? : }
{ setShowFocusMode(true); setShowMobileRail(false); setFlyoutSection(null); }} style={{ ...railIconBtnStyle }} title={profile.language === "de" ? "Fokus" : "Focus"}>
{railSep}
@@ -6331,6 +6340,7 @@ export default function WeeklyView() {
{ handleRedo(); setShowHeaderMore(false); }} disabled={redoCount === 0}> Redo
{ const now = new Date(); setCalendarEventModal({ isOpen: true, event: undefined, initialDate: now, initialStartTime: `${String(now.getHours()).padStart(2, "0")}:00` }); setShowHeaderMore(false); }}> {profile.language === "de" ? "Termin erstellen" : "Add Event"}
{ setIsRecurringTasksOpen(true); setShowHeaderMore(false); }}> {profile.language === "de" ? "Wiederkehrend" : "Recurring"}
+ { setIsOverdueTasksOpen(true); setShowHeaderMore(false); }}> {profile.language === "de" ? "Überfällig" : "Overdue"}{overdueCount > 0 ? ` (${overdueCount})` : ""}
{ const nv = !showNextTask; setShowNextTask(nv); saveSetting("showNextTask", nv); setShowHeaderMore(false); }}>{showNextTask ? : } {showNextTask ? "Next Task" : "Goal"}
{ setShowFocusMode(true); setShowHeaderMore(false); }}> {profile.language === "de" ? "Fokus" : "Focus"}
{ setDarkMode(!darkMode); setShowHeaderMore(false); }}>{darkMode ? : } {darkMode ? "Light" : "Dark"}
@@ -6363,6 +6373,7 @@ export default function WeeklyView() {
{!useLeftRail && (
<>
setIsRecurringTasksOpen(true)} title="Recurring Tasks">
+ setIsOverdueTasksOpen(true)} title={overdueCount > 0 ? `Overdue (${overdueCount})` : "Overdue"}>
setShowProjectsSidebar(true)} title="New Project">
setShowWeekPrintModal(true)} title={profile.language === "de" ? "Woche drucken / exportieren" : "Print / export week"}>
>
@@ -8910,6 +8921,24 @@ export default function WeeklyView() {
onSeriesUpdated={() => fetchTasks()}
/>
+ setIsOverdueTasksOpen(false)}
+ tasks={tasks}
+ language={profile.language}
+ onToggleComplete={toggleTask}
+ onMoveToToday={(task) => {
+ const today = new Date();
+ moveTaskToSlot(task.id, today.getDay(), task.startTime || "09:00", today);
+ }}
+ onMoveAllToToday={(tasksToMove) => {
+ const today = new Date();
+ for (const task of tasksToMove) {
+ moveTaskToSlot(task.id, today.getDay(), task.startTime || "09:00", today);
+ }
+ }}
+ />
+
{/* Recurring Task Delete Confirmation Modal */}
{
recurringDeleteModal.isOpen && (
diff --git a/src/lib/weekly/overdue.ts b/src/lib/weekly/overdue.ts
new file mode 100644
index 0000000..86bae30
--- /dev/null
+++ b/src/lib/weekly/overdue.ts
@@ -0,0 +1,54 @@
+import type { Task } from "@/lib/weekly/types";
+
+/**
+ * Überfällige Aufgaben: in der Vergangenheit geplant, nicht erledigt.
+ *
+ * Reine Funktionen, damit sie ohne Browser/DOM testbar sind -- siehe
+ * src/components/OverdueTasksManager.tsx, das sie zum Rendern verwendet. Datum-Vergleich
+ * als String (YYYY-MM-DD), damit Uhrzeiten und Zeitzonen keine Rolle spielen: ein Task von
+ * "heute Nachmittag, noch nicht erledigt" ist nicht überfällig, "gestern" schon.
+ */
+
+function localDayString(date: Date): string {
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, "0");
+ const day = String(date.getDate()).padStart(2, "0");
+ return `${year}-${month}-${day}`;
+}
+
+/**
+ * Nur Aufgaben mit `isRolling` oder bei global aktiviertem `autoRolling` wandern
+ * automatisch auf heute (siehe rollOverdueTasks in WeeklyView.tsx) -- diese Funktion
+ * erfasst absichtlich ALLE unerledigten überfälligen Aufgaben, unabhängig vom
+ * Rolling-Status, damit nichts unbemerkt auf einem vergangenen Datum liegen bleibt.
+ * Someday-Aufgaben zählen nicht mit: die stehen bewusst ohne festes Datum.
+ */
+export function getOverdueTasks(tasks: Task[], now: Date = new Date()): Task[] {
+ const todayStr = localDayString(now);
+ return tasks.filter(
+ (t) =>
+ !t.completed &&
+ !t.somedayListId &&
+ !!t.scheduledDate &&
+ localDayString(new Date(t.scheduledDate)) < todayStr,
+ );
+}
+
+export interface OverdueGroup {
+ dateStr: string;
+ tasks: Task[];
+}
+
+/** Gruppiert nach dem ursprünglich geplanten Tag, älteste Gruppe zuerst. */
+export function groupOverdueByDate(tasks: Task[]): OverdueGroup[] {
+ const groups = new Map();
+ for (const task of tasks) {
+ const dateStr = localDayString(new Date(task.scheduledDate as string));
+ const bucket = groups.get(dateStr);
+ if (bucket) bucket.push(task);
+ else groups.set(dateStr, [task]);
+ }
+ return Array.from(groups.entries())
+ .sort(([a], [b]) => a.localeCompare(b))
+ .map(([dateStr, tasks]) => ({ dateStr, tasks }));
+}
diff --git a/tests/unit/overdue.test.ts b/tests/unit/overdue.test.ts
new file mode 100644
index 0000000..a1b19c9
--- /dev/null
+++ b/tests/unit/overdue.test.ts
@@ -0,0 +1,90 @@
+import { test, describe } from "node:test";
+import assert from "node:assert/strict";
+import { getOverdueTasks, groupOverdueByDate } from "../../src/lib/weekly/overdue.ts";
+import type { Task } from "../../src/lib/weekly/types.ts";
+
+/**
+ * Erkennung und Gruppierung überfälliger Aufgaben. Beobachtbares Verhalten: eine
+ * unerledigte Aufgabe mit einem Datum vor heute gilt als überfällig, unabhängig vom
+ * Rolling-Status (siehe OverdueTasksManager.tsx für den fachlichen Hintergrund) --
+ * Someday-Aufgaben und heute fällige Aufgaben zählen bewusst nicht mit.
+ */
+
+const baseTask: Task = {
+ id: "t1",
+ title: "Test task",
+ completed: false,
+ order: 0,
+ updatedAt: new Date(),
+ userId: "u1",
+};
+
+const now = new Date(2026, 8, 14, 10, 0, 0); // Montag, 14.09.2026, 10 Uhr
+
+describe("getOverdueTasks", () => {
+ test("unerledigte Aufgabe mit vergangenem Datum ist überfällig", () => {
+ const task = { ...baseTask, scheduledDate: "2026-09-10" };
+ assert.deepEqual(getOverdueTasks([task], now), [task]);
+ });
+
+ test("erledigte Aufgabe mit vergangenem Datum ist nicht überfällig", () => {
+ const task = { ...baseTask, scheduledDate: "2026-09-10", completed: true };
+ assert.deepEqual(getOverdueTasks([task], now), []);
+ });
+
+ test("heute fällige Aufgabe ist nicht überfällig, auch nachmittags nicht", () => {
+ const task = { ...baseTask, scheduledDate: "2026-09-14" };
+ assert.deepEqual(getOverdueTasks([task], now), []);
+ });
+
+ test("zukünftige Aufgabe ist nicht überfällig", () => {
+ const task = { ...baseTask, scheduledDate: "2026-09-20" };
+ assert.deepEqual(getOverdueTasks([task], now), []);
+ });
+
+ test("Aufgabe ohne Datum ist nicht überfällig", () => {
+ const task = { ...baseTask, scheduledDate: null };
+ assert.deepEqual(getOverdueTasks([task], now), []);
+ });
+
+ test("Someday-Aufgabe zählt nicht mit, selbst mit vergangenem Datum", () => {
+ const task = { ...baseTask, scheduledDate: "2026-09-10", somedayListId: "list1" };
+ assert.deepEqual(getOverdueTasks([task], now), []);
+ });
+
+ test("virtuelle wiederkehrende Instanz mit vergangenem Datum zählt mit", () => {
+ // Der Server materialisiert virtuelle Ids bei jedem PATCH (siehe
+ // VIRTUAL_ID_REGEX in api/tasks/route.ts) -- toggleTask/moveTaskToSlot
+ // funktionieren darauf bereits unverändert.
+ const task = { ...baseTask, id: "virtual-t1-2026-09-10", scheduledDate: "2026-09-10" };
+ assert.deepEqual(getOverdueTasks([task], now), [task]);
+ });
+
+ test("mehrere Aufgaben werden korrekt gefiltert", () => {
+ const overdueTask = { ...baseTask, id: "a", scheduledDate: "2026-09-10" };
+ const futureTask = { ...baseTask, id: "b", scheduledDate: "2026-09-20" };
+ const doneTask = { ...baseTask, id: "c", scheduledDate: "2026-09-01", completed: true };
+ assert.deepEqual(getOverdueTasks([overdueTask, futureTask, doneTask], now), [overdueTask]);
+ });
+});
+
+describe("groupOverdueByDate", () => {
+ test("gruppiert nach Datum, älteste Gruppe zuerst", () => {
+ const a = { ...baseTask, id: "a", scheduledDate: "2026-09-12" };
+ const b = { ...baseTask, id: "b", scheduledDate: "2026-09-10" };
+ const c = { ...baseTask, id: "c", scheduledDate: "2026-09-12" };
+
+ const groups = groupOverdueByDate([a, b, c]);
+
+ assert.deepEqual(
+ groups.map((g) => g.dateStr),
+ ["2026-09-10", "2026-09-12"],
+ );
+ assert.deepEqual(groups[0].tasks, [b]);
+ assert.deepEqual(groups[1].tasks, [a, c]);
+ });
+
+ test("leere Liste ergibt keine Gruppen", () => {
+ assert.deepEqual(groupOverdueByDate([]), []);
+ });
+});