feat: Uebersicht ueberfaelliger Aufgaben
Aufgaben, die in der Vergangenheit geplant und nicht erledigt wurden, waren bislang praktisch unsichtbar: nur Aufgaben mit aktiviertem Rolling-Schalter (oder bei global aktiviertem Auto-Rolling) wandern automatisch auf heute (rollOverdueTasks in WeeklyView.tsx) -- alles andere blieb fuer immer auf seinem urspruenglichen Datum stehen, sobald man in der Wochenansicht weitergeklickt hat. - src/lib/weekly/overdue.ts: reine Funktionen getOverdueTasks/groupOverdueByDate (unerledigt, kein Someday, scheduledDate vor heute -- Datumsvergleich als String, damit Uhrzeit/Zeitzone keine Rolle spielen). 10 Unit-Tests. - src/components/OverdueTasksManager.tsx: Dialog, gruppiert nach dem urspruenglich geplanten Tag (aelteste zuerst), Checkbox zum Erledigen, Knopf zum Verschieben auf heute pro Aufgabe, "Alle auf heute verschieben". Nutzt LinkifiedText (v1.128.0) -- Titel mit URLs bleiben auch hier anklickbar. Verschieben/Erledigen laeuft ueber die bestehenden moveTaskToSlot/toggleTask-Pfade, keine neue API. - Neuer Menuepunkt "Ueberfaellig" mit Anzahl-Badge neben "Wiederkehrend" in allen bestehenden Menue-Varianten (linke/obere Randleiste, mobile Leiste, Kopfzeile). Verifiziert nicht nur mit Unit-Tests: lokale Instanz mit Wegwerf-Postgres (Docker) aufgesetzt, drei ueberfaellige Aufgaben geseedet (eine mit URL im Titel), per Playwright eingeloggt -- Badge, Gruppierung und Link-Rendering per Screenshot geprueft. Danach real geklickt: eine Aufgabe per Checkbox erledigt, eine per Knopf auf heute verschoben, die restlichen zwei per Sammelknopf -- alle drei Aenderungen per Datenbankabfrage bestaetigt. Ausserdem: Typecheck, Lint (0 Fehler), 118 Unit-Tests (10 neu), Produktions-Build. v1.129.0 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
7f850bb4e9
commit
64f0eb73f8
27
CHANGELOG.md
27
CHANGELOG.md
@ -2,6 +2,33 @@
|
|||||||
|
|
||||||
All notable changes to this project are documented in this file.
|
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
|
## [1.128.0] - 2026-09-14
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "my-weekly-todo-list",
|
"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",
|
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
139
src/components/OverdueTasksManager.tsx
Normal file
139
src/components/OverdueTasksManager.tsx
Normal file
@ -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 (
|
||||||
|
<div className="fixed inset-0 bg-black/50 z-[3000] flex items-center justify-center p-4 animate-fadeIn" onClick={onClose}>
|
||||||
|
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-2xl w-[600px] max-w-full overflow-hidden flex flex-col animate-scaleIn" onClick={(e) => e.stopPropagation()} style={{ maxHeight: "80vh" }}>
|
||||||
|
<div className="p-5 flex items-center justify-between border-b border-gray-100 dark:border-gray-800">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="p-2 bg-red-100 dark:bg-red-900/30 rounded-lg text-red-600">
|
||||||
|
<AlertCircle size={20} />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-lg font-bold text-gray-800 dark:text-white">
|
||||||
|
{de ? "Überfällig" : "Overdue"}
|
||||||
|
{overdue.length > 0 && (
|
||||||
|
<span className="ml-2 text-sm font-medium text-gray-400">({overdue.length})</span>
|
||||||
|
)}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<button onClick={onClose} className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full text-gray-400 transition-colors">
|
||||||
|
<X size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto p-6">
|
||||||
|
{groups.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||||
|
<div className="w-16 h-16 bg-gray-50 dark:bg-gray-800 rounded-full flex items-center justify-center text-gray-300 mb-4">
|
||||||
|
<AlertCircle size={32} />
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-500 dark:text-gray-400 max-w-xs">
|
||||||
|
{de ? "Nichts Überfälliges — alles erledigt oder rechtzeitig verschoben." : "Nothing overdue — all caught up."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-5">
|
||||||
|
{groups.map(({ dateStr, tasks: dayTasks }) => (
|
||||||
|
<div key={dateStr}>
|
||||||
|
<div className="text-xs font-bold uppercase tracking-wide text-gray-400 dark:text-gray-500 mb-2">
|
||||||
|
{dateLabel(dateStr)}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{dayTasks.map((task) => (
|
||||||
|
<div
|
||||||
|
key={task.id}
|
||||||
|
className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-800/50 rounded-xl border border-gray-100 dark:border-gray-700/50 hover:border-red-200 dark:hover:border-red-900/50 transition-all"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={false}
|
||||||
|
onChange={() => onToggleComplete(task.id)}
|
||||||
|
className="w-4 h-4 flex-shrink-0 cursor-pointer"
|
||||||
|
aria-label={de ? `"${task.title}" erledigt` : `Mark "${task.title}" complete`}
|
||||||
|
/>
|
||||||
|
<div className="flex-1 min-w-0 text-sm text-gray-800 dark:text-gray-100 truncate" title={task.title}>
|
||||||
|
<LinkifiedText text={task.title} />
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => 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"}
|
||||||
|
>
|
||||||
|
<ArrowRight size={12} />
|
||||||
|
{de ? "Heute" : "Today"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{overdue.length > 0 && (
|
||||||
|
<div className="p-3 bg-gray-50 dark:bg-gray-900 border-t border-gray-100 dark:border-gray-800 flex justify-center">
|
||||||
|
<button
|
||||||
|
onClick={() => 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"
|
||||||
|
>
|
||||||
|
<ArrowRightCircle size={16} />
|
||||||
|
{de ? "Alle auf heute verschieben" : "Move all to today"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -116,6 +116,8 @@ import UserMenu from "./UserMenu";
|
|||||||
import SearchModal from "./SearchModal";
|
import SearchModal from "./SearchModal";
|
||||||
import SimpleDatePicker from "./SimpleDatePicker";
|
import SimpleDatePicker from "./SimpleDatePicker";
|
||||||
import RecurringTasksManager from "./RecurringTasksManager";
|
import RecurringTasksManager from "./RecurringTasksManager";
|
||||||
|
import OverdueTasksManager from "./OverdueTasksManager";
|
||||||
|
import { getOverdueTasks } from "@/lib/weekly/overdue";
|
||||||
import WeekPrintModal from "./WeekPrintModal";
|
import WeekPrintModal from "./WeekPrintModal";
|
||||||
import { ProjectIcon } from "./weekly/ProjectIcon";
|
import { ProjectIcon } from "./weekly/ProjectIcon";
|
||||||
import { ProjectsSidebar } from "./weekly/ProjectsSidebar";
|
import { ProjectsSidebar } from "./weekly/ProjectsSidebar";
|
||||||
@ -936,6 +938,8 @@ export default function WeeklyView() {
|
|||||||
// New UI State
|
// New UI State
|
||||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||||
const [isRecurringTasksOpen, setIsRecurringTasksOpen] = 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 [showDatePicker, setShowDatePicker] = useState(false);
|
||||||
const datePickerBtnRef = useRef<HTMLDivElement>(null);
|
const datePickerBtnRef = useRef<HTMLDivElement>(null);
|
||||||
const [showNavDatePicker, setShowNavDatePicker] = useState(false);
|
const [showNavDatePicker, setShowNavDatePicker] = useState(false);
|
||||||
@ -5494,6 +5498,7 @@ export default function WeeklyView() {
|
|||||||
<button onClick={() => { const now = new Date(); setCalendarEventModal({ isOpen: true, event: undefined, initialDate: now, initialStartTime: `${String(now.getHours()).padStart(2, "0")}:00` }); setShowQuickSettings(false); }} style={qsActionStyle(darkMode)}><Plus size={16} /> <span>{profile.language === "de" ? "Kalendereintrag" : "Calendar Event"}</span></button>
|
<button onClick={() => { const now = new Date(); setCalendarEventModal({ isOpen: true, event: undefined, initialDate: now, initialStartTime: `${String(now.getHours()).padStart(2, "0")}:00` }); setShowQuickSettings(false); }} style={qsActionStyle(darkMode)}><Plus size={16} /> <span>{profile.language === "de" ? "Kalendereintrag" : "Calendar Event"}</span></button>
|
||||||
<button onClick={() => { setShowProjectsSidebar(true); setShowQuickSettings(false); }} style={qsActionStyle(darkMode)}><FolderPlus size={16} /> <span>{profile.language === "de" ? "Projekte" : "Projects"}</span></button>
|
<button onClick={() => { setShowProjectsSidebar(true); setShowQuickSettings(false); }} style={qsActionStyle(darkMode)}><FolderPlus size={16} /> <span>{profile.language === "de" ? "Projekte" : "Projects"}</span></button>
|
||||||
<button onClick={() => { setIsRecurringTasksOpen(true); setShowQuickSettings(false); }} style={qsActionStyle(darkMode)}><Repeat size={16} /> <span>{profile.language === "de" ? "Wiederkehrend" : "Recurring Tasks"}</span></button>
|
<button onClick={() => { setIsRecurringTasksOpen(true); setShowQuickSettings(false); }} style={qsActionStyle(darkMode)}><Repeat size={16} /> <span>{profile.language === "de" ? "Wiederkehrend" : "Recurring Tasks"}</span></button>
|
||||||
|
<button onClick={() => { setIsOverdueTasksOpen(true); setShowQuickSettings(false); }} style={qsActionStyle(darkMode)}><AlertCircle size={16} /> <span>{profile.language === "de" ? "Überfällig" : "Overdue"}{overdueCount > 0 ? ` (${overdueCount})` : ""}</span></button>
|
||||||
<button onClick={() => { const nv = !profile.showNextTask; setShowNextTask(nv); saveSetting("showNextTask", nv); }} style={qsActionStyle(darkMode)}>{profile.showNextTask ? <Play size={16} /> : <Target size={16} />}<span>{profile.showNextTask ? (profile.language === "de" ? "Nächste Aufgabe" : "Next Task") : (profile.language === "de" ? "Ziel" : "Goal")}</span></button>
|
<button onClick={() => { const nv = !profile.showNextTask; setShowNextTask(nv); saveSetting("showNextTask", nv); }} style={qsActionStyle(darkMode)}>{profile.showNextTask ? <Play size={16} /> : <Target size={16} />}<span>{profile.showNextTask ? (profile.language === "de" ? "Nächste Aufgabe" : "Next Task") : (profile.language === "de" ? "Ziel" : "Goal")}</span></button>
|
||||||
<button onClick={() => { setShowFocusMode(true); setShowQuickSettings(false); }} style={qsActionStyle(darkMode)}><Zap size={16} /> <span>{profile.language === "de" ? "Fokus" : "Focus"}</span></button>
|
<button onClick={() => { setShowFocusMode(true); setShowQuickSettings(false); }} style={qsActionStyle(darkMode)}><Zap size={16} /> <span>{profile.language === "de" ? "Fokus" : "Focus"}</span></button>
|
||||||
</div>
|
</div>
|
||||||
@ -5558,6 +5563,7 @@ export default function WeeklyView() {
|
|||||||
<button onClick={() => { const now = new Date(); setCalendarEventModal({ isOpen: true, event: undefined, initialDate: now, initialStartTime: `${String(now.getHours()).padStart(2, "0")}:00` }); setShowQuickSettings(false); }} style={qsActionStyle(darkMode)}><Plus size={16} /> <span>{profile.language === "de" ? "Kalendereintrag" : "Calendar Event"}</span></button>
|
<button onClick={() => { const now = new Date(); setCalendarEventModal({ isOpen: true, event: undefined, initialDate: now, initialStartTime: `${String(now.getHours()).padStart(2, "0")}:00` }); setShowQuickSettings(false); }} style={qsActionStyle(darkMode)}><Plus size={16} /> <span>{profile.language === "de" ? "Kalendereintrag" : "Calendar Event"}</span></button>
|
||||||
<button onClick={() => { setShowProjectsSidebar(true); setShowQuickSettings(false); }} style={qsActionStyle(darkMode)}><FolderPlus size={16} /> <span>{profile.language === "de" ? "Projekte" : "Projects"}</span></button>
|
<button onClick={() => { setShowProjectsSidebar(true); setShowQuickSettings(false); }} style={qsActionStyle(darkMode)}><FolderPlus size={16} /> <span>{profile.language === "de" ? "Projekte" : "Projects"}</span></button>
|
||||||
<button onClick={() => { setIsRecurringTasksOpen(true); setShowQuickSettings(false); }} style={qsActionStyle(darkMode)}><Repeat size={16} /> <span>{profile.language === "de" ? "Wiederkehrend" : "Recurring Tasks"}</span></button>
|
<button onClick={() => { setIsRecurringTasksOpen(true); setShowQuickSettings(false); }} style={qsActionStyle(darkMode)}><Repeat size={16} /> <span>{profile.language === "de" ? "Wiederkehrend" : "Recurring Tasks"}</span></button>
|
||||||
|
<button onClick={() => { setIsOverdueTasksOpen(true); setShowQuickSettings(false); }} style={qsActionStyle(darkMode)}><AlertCircle size={16} /> <span>{profile.language === "de" ? "Überfällig" : "Overdue"}{overdueCount > 0 ? ` (${overdueCount})` : ""}</span></button>
|
||||||
<button onClick={() => { const nv = !profile.showNextTask; setShowNextTask(nv); saveSetting("showNextTask", nv); }} style={qsActionStyle(darkMode)}>{profile.showNextTask ? <Play size={16} /> : <Target size={16} />}<span>{profile.showNextTask ? (profile.language === "de" ? "Nächste Aufgabe" : "Next Task") : (profile.language === "de" ? "Ziel" : "Goal")}</span></button>
|
<button onClick={() => { const nv = !profile.showNextTask; setShowNextTask(nv); saveSetting("showNextTask", nv); }} style={qsActionStyle(darkMode)}>{profile.showNextTask ? <Play size={16} /> : <Target size={16} />}<span>{profile.showNextTask ? (profile.language === "de" ? "Nächste Aufgabe" : "Next Task") : (profile.language === "de" ? "Ziel" : "Goal")}</span></button>
|
||||||
<button onClick={() => { setShowFocusMode(true); setShowQuickSettings(false); }} style={qsActionStyle(darkMode)}><Zap size={16} /> <span>{profile.language === "de" ? "Fokus" : "Focus"}</span></button>
|
<button onClick={() => { setShowFocusMode(true); setShowQuickSettings(false); }} style={qsActionStyle(darkMode)}><Zap size={16} /> <span>{profile.language === "de" ? "Fokus" : "Focus"}</span></button>
|
||||||
</div>
|
</div>
|
||||||
@ -5623,6 +5629,7 @@ export default function WeeklyView() {
|
|||||||
<button onClick={() => { const now = new Date(); setCalendarEventModal({ isOpen: true, event: undefined, initialDate: now, initialStartTime: `${String(now.getHours()).padStart(2, "0")}:00` }); }} style={qsActionStyle(darkMode)}><Plus size={16} /> <span>{profile.language === "de" ? "Kalendereintrag" : "Calendar Event"}</span></button>
|
<button onClick={() => { const now = new Date(); setCalendarEventModal({ isOpen: true, event: undefined, initialDate: now, initialStartTime: `${String(now.getHours()).padStart(2, "0")}:00` }); }} style={qsActionStyle(darkMode)}><Plus size={16} /> <span>{profile.language === "de" ? "Kalendereintrag" : "Calendar Event"}</span></button>
|
||||||
<button onClick={() => setShowProjectsSidebar(true)} style={qsActionStyle(darkMode)}><FolderPlus size={16} /> <span>{profile.language === "de" ? "Projekte" : "Projects"}</span></button>
|
<button onClick={() => setShowProjectsSidebar(true)} style={qsActionStyle(darkMode)}><FolderPlus size={16} /> <span>{profile.language === "de" ? "Projekte" : "Projects"}</span></button>
|
||||||
<button onClick={() => setIsRecurringTasksOpen(true)} style={qsActionStyle(darkMode)}><Repeat size={16} /> <span>{profile.language === "de" ? "Wiederkehrend" : "Recurring Tasks"}</span></button>
|
<button onClick={() => setIsRecurringTasksOpen(true)} style={qsActionStyle(darkMode)}><Repeat size={16} /> <span>{profile.language === "de" ? "Wiederkehrend" : "Recurring Tasks"}</span></button>
|
||||||
|
<button onClick={() => setIsOverdueTasksOpen(true)} style={qsActionStyle(darkMode)}><AlertCircle size={16} /> <span>{profile.language === "de" ? "Überfällig" : "Overdue"}{overdueCount > 0 ? ` (${overdueCount})` : ""}</span></button>
|
||||||
<button onClick={() => { const nv = !profile.showNextTask; setShowNextTask(nv); saveSetting("showNextTask", nv); }} style={qsActionStyle(darkMode)}>{profile.showNextTask ? <Play size={16} /> : <Target size={16} />}<span>{profile.showNextTask ? (profile.language === "de" ? "Nächste Aufgabe" : "Next Task") : (profile.language === "de" ? "Ziel" : "Goal")}</span></button>
|
<button onClick={() => { const nv = !profile.showNextTask; setShowNextTask(nv); saveSetting("showNextTask", nv); }} style={qsActionStyle(darkMode)}>{profile.showNextTask ? <Play size={16} /> : <Target size={16} />}<span>{profile.showNextTask ? (profile.language === "de" ? "Nächste Aufgabe" : "Next Task") : (profile.language === "de" ? "Ziel" : "Goal")}</span></button>
|
||||||
<button onClick={() => setShowFocusMode(true)} style={qsActionStyle(darkMode)}><Zap size={16} /> <span>{profile.language === "de" ? "Fokus" : "Focus"}</span></button>
|
<button onClick={() => setShowFocusMode(true)} style={qsActionStyle(darkMode)}><Zap size={16} /> <span>{profile.language === "de" ? "Fokus" : "Focus"}</span></button>
|
||||||
</div>
|
</div>
|
||||||
@ -5683,6 +5690,7 @@ export default function WeeklyView() {
|
|||||||
<button onClick={() => { 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"}><Plus size={15} /></button>
|
<button onClick={() => { 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"}><Plus size={15} /></button>
|
||||||
<button onClick={() => setShowProjectsSidebar(true)} style={{ ...railIconBtnStyle }} title={profile.language === "de" ? "Projekte" : "Projects"}><FolderOpen size={15} /></button>
|
<button onClick={() => setShowProjectsSidebar(true)} style={{ ...railIconBtnStyle }} title={profile.language === "de" ? "Projekte" : "Projects"}><FolderOpen size={15} /></button>
|
||||||
<button onClick={() => setIsRecurringTasksOpen(true)} style={{ ...railIconBtnStyle }} title={profile.language === "de" ? "Wiederkehrend" : "Recurring"}><Repeat size={15} /></button>
|
<button onClick={() => setIsRecurringTasksOpen(true)} style={{ ...railIconBtnStyle }} title={profile.language === "de" ? "Wiederkehrend" : "Recurring"}><Repeat size={15} /></button>
|
||||||
|
<button onClick={() => setIsOverdueTasksOpen(true)} style={{ ...railIconBtnStyle }} title={`${profile.language === "de" ? "Überfällig" : "Overdue"}${overdueCount > 0 ? ` (${overdueCount})` : ""}`}><AlertCircle size={15} /></button>
|
||||||
<button onClick={() => { const nv = !profile.showNextTask; setShowNextTask(nv); saveSetting("showNextTask", nv); }} style={{ ...railIconBtnStyle }} title={profile.language === "de" ? "Ziel" : "Goal"}>{profile.showNextTask ? <Play size={15} /> : <Target size={15} />}</button>
|
<button onClick={() => { const nv = !profile.showNextTask; setShowNextTask(nv); saveSetting("showNextTask", nv); }} style={{ ...railIconBtnStyle }} title={profile.language === "de" ? "Ziel" : "Goal"}>{profile.showNextTask ? <Play size={15} /> : <Target size={15} />}</button>
|
||||||
<button onClick={() => setShowFocusMode(true)} style={{ ...railIconBtnStyle }} title={profile.language === "de" ? "Fokus" : "Focus"}><Zap size={15} /></button>
|
<button onClick={() => setShowFocusMode(true)} style={{ ...railIconBtnStyle }} title={profile.language === "de" ? "Fokus" : "Focus"}><Zap size={15} /></button>
|
||||||
{railSep}
|
{railSep}
|
||||||
@ -5922,6 +5930,7 @@ export default function WeeklyView() {
|
|||||||
<button className="mobile-rail-btn" onClick={() => { 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"}><Plus size={15} /></button>
|
<button className="mobile-rail-btn" onClick={() => { 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"}><Plus size={15} /></button>
|
||||||
<button className="mobile-rail-btn" onClick={() => { setShowProjectsSidebar(true); setShowMobileRail(false); setFlyoutSection(null); }} style={{ ...railIconBtnStyle }} title={profile.language === "de" ? "Projekte" : "Projects"}><FolderOpen size={15} /></button>
|
<button className="mobile-rail-btn" onClick={() => { setShowProjectsSidebar(true); setShowMobileRail(false); setFlyoutSection(null); }} style={{ ...railIconBtnStyle }} title={profile.language === "de" ? "Projekte" : "Projects"}><FolderOpen size={15} /></button>
|
||||||
<button className="mobile-rail-btn" onClick={() => { setIsRecurringTasksOpen(true); setShowMobileRail(false); setFlyoutSection(null); }} style={{ ...railIconBtnStyle }} title={profile.language === "de" ? "Wiederkehrend" : "Recurring"}><Repeat size={15} /></button>
|
<button className="mobile-rail-btn" onClick={() => { setIsRecurringTasksOpen(true); setShowMobileRail(false); setFlyoutSection(null); }} style={{ ...railIconBtnStyle }} title={profile.language === "de" ? "Wiederkehrend" : "Recurring"}><Repeat size={15} /></button>
|
||||||
|
<button className="mobile-rail-btn" onClick={() => { setIsOverdueTasksOpen(true); setShowMobileRail(false); setFlyoutSection(null); }} style={{ ...railIconBtnStyle }} title={`${profile.language === "de" ? "Überfällig" : "Overdue"}${overdueCount > 0 ? ` (${overdueCount})` : ""}`}><AlertCircle size={15} /></button>
|
||||||
<button className="mobile-rail-btn" onClick={() => { const nv = !profile.showNextTask; setShowNextTask(nv); saveSetting("showNextTask", nv); }} style={{ ...railIconBtnStyle }} title={profile.language === "de" ? "Ziel" : "Goal"}>{profile.showNextTask ? <Play size={15} /> : <Target size={15} />}</button>
|
<button className="mobile-rail-btn" onClick={() => { const nv = !profile.showNextTask; setShowNextTask(nv); saveSetting("showNextTask", nv); }} style={{ ...railIconBtnStyle }} title={profile.language === "de" ? "Ziel" : "Goal"}>{profile.showNextTask ? <Play size={15} /> : <Target size={15} />}</button>
|
||||||
<button className="mobile-rail-btn" onClick={() => { setShowFocusMode(true); setShowMobileRail(false); setFlyoutSection(null); }} style={{ ...railIconBtnStyle }} title={profile.language === "de" ? "Fokus" : "Focus"}><Zap size={15} /></button>
|
<button className="mobile-rail-btn" onClick={() => { setShowFocusMode(true); setShowMobileRail(false); setFlyoutSection(null); }} style={{ ...railIconBtnStyle }} title={profile.language === "de" ? "Fokus" : "Focus"}><Zap size={15} /></button>
|
||||||
{railSep}
|
{railSep}
|
||||||
@ -6331,6 +6340,7 @@ export default function WeeklyView() {
|
|||||||
<button onClick={() => { handleRedo(); setShowHeaderMore(false); }} disabled={redoCount === 0}><Redo2 size={16} /> <span>Redo</span></button>
|
<button onClick={() => { handleRedo(); setShowHeaderMore(false); }} disabled={redoCount === 0}><Redo2 size={16} /> <span>Redo</span></button>
|
||||||
<button onClick={() => { const now = new Date(); setCalendarEventModal({ isOpen: true, event: undefined, initialDate: now, initialStartTime: `${String(now.getHours()).padStart(2, "0")}:00` }); setShowHeaderMore(false); }}><Plus size={16} /> <span>{profile.language === "de" ? "Termin erstellen" : "Add Event"}</span></button>
|
<button onClick={() => { const now = new Date(); setCalendarEventModal({ isOpen: true, event: undefined, initialDate: now, initialStartTime: `${String(now.getHours()).padStart(2, "0")}:00` }); setShowHeaderMore(false); }}><Plus size={16} /> <span>{profile.language === "de" ? "Termin erstellen" : "Add Event"}</span></button>
|
||||||
<button onClick={() => { setIsRecurringTasksOpen(true); setShowHeaderMore(false); }}><Repeat size={16} /> <span>{profile.language === "de" ? "Wiederkehrend" : "Recurring"}</span></button>
|
<button onClick={() => { setIsRecurringTasksOpen(true); setShowHeaderMore(false); }}><Repeat size={16} /> <span>{profile.language === "de" ? "Wiederkehrend" : "Recurring"}</span></button>
|
||||||
|
<button onClick={() => { setIsOverdueTasksOpen(true); setShowHeaderMore(false); }}><AlertCircle size={16} /> <span>{profile.language === "de" ? "Überfällig" : "Overdue"}{overdueCount > 0 ? ` (${overdueCount})` : ""}</span></button>
|
||||||
<button onClick={() => { const nv = !showNextTask; setShowNextTask(nv); saveSetting("showNextTask", nv); setShowHeaderMore(false); }}>{showNextTask ? <Play size={16} /> : <Target size={16} />} <span>{showNextTask ? "Next Task" : "Goal"}</span></button>
|
<button onClick={() => { const nv = !showNextTask; setShowNextTask(nv); saveSetting("showNextTask", nv); setShowHeaderMore(false); }}>{showNextTask ? <Play size={16} /> : <Target size={16} />} <span>{showNextTask ? "Next Task" : "Goal"}</span></button>
|
||||||
<button onClick={() => { setShowFocusMode(true); setShowHeaderMore(false); }}><Zap size={16} /> <span>{profile.language === "de" ? "Fokus" : "Focus"}</span></button>
|
<button onClick={() => { setShowFocusMode(true); setShowHeaderMore(false); }}><Zap size={16} /> <span>{profile.language === "de" ? "Fokus" : "Focus"}</span></button>
|
||||||
<button onClick={() => { setDarkMode(!darkMode); setShowHeaderMore(false); }}>{darkMode ? <Sun size={16} /> : <Moon size={16} />} <span>{darkMode ? "Light" : "Dark"}</span></button>
|
<button onClick={() => { setDarkMode(!darkMode); setShowHeaderMore(false); }}>{darkMode ? <Sun size={16} /> : <Moon size={16} />} <span>{darkMode ? "Light" : "Dark"}</span></button>
|
||||||
@ -6363,6 +6373,7 @@ export default function WeeklyView() {
|
|||||||
{!useLeftRail && (
|
{!useLeftRail && (
|
||||||
<>
|
<>
|
||||||
<button className="weekly-btn-icon header-desktop-only" onClick={() => setIsRecurringTasksOpen(true)} title="Recurring Tasks"><Repeat size={17} /></button>
|
<button className="weekly-btn-icon header-desktop-only" onClick={() => setIsRecurringTasksOpen(true)} title="Recurring Tasks"><Repeat size={17} /></button>
|
||||||
|
<button className="weekly-btn-icon header-desktop-only" onClick={() => setIsOverdueTasksOpen(true)} title={overdueCount > 0 ? `Overdue (${overdueCount})` : "Overdue"}><AlertCircle size={17} /></button>
|
||||||
<button className="weekly-btn-icon header-desktop-only" onClick={() => setShowProjectsSidebar(true)} title="New Project"><FolderPlus size={17} /></button>
|
<button className="weekly-btn-icon header-desktop-only" onClick={() => setShowProjectsSidebar(true)} title="New Project"><FolderPlus size={17} /></button>
|
||||||
<button className="weekly-btn-icon header-desktop-only" onClick={() => setShowWeekPrintModal(true)} title={profile.language === "de" ? "Woche drucken / exportieren" : "Print / export week"}><Printer size={17} /></button>
|
<button className="weekly-btn-icon header-desktop-only" onClick={() => setShowWeekPrintModal(true)} title={profile.language === "de" ? "Woche drucken / exportieren" : "Print / export week"}><Printer size={17} /></button>
|
||||||
</>
|
</>
|
||||||
@ -8910,6 +8921,24 @@ export default function WeeklyView() {
|
|||||||
onSeriesUpdated={() => fetchTasks()}
|
onSeriesUpdated={() => fetchTasks()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<OverdueTasksManager
|
||||||
|
isOpen={isOverdueTasksOpen}
|
||||||
|
onClose={() => 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 */}
|
{/* Recurring Task Delete Confirmation Modal */}
|
||||||
{
|
{
|
||||||
recurringDeleteModal.isOpen && (
|
recurringDeleteModal.isOpen && (
|
||||||
|
|||||||
54
src/lib/weekly/overdue.ts
Normal file
54
src/lib/weekly/overdue.ts
Normal file
@ -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<string, Task[]>();
|
||||||
|
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 }));
|
||||||
|
}
|
||||||
90
tests/unit/overdue.test.ts
Normal file
90
tests/unit/overdue.test.ts
Normal file
@ -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([]), []);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user