refactor: Datums- und Zeitraster-Helfer nach src/lib/weekly/date-utils.ts
Some checks are pending
Playwright Tests / test (push) Waiting to run
Test and SonarQube / quality (push) Waiting to run

Zweiter Schnitt an WeeklyView.tsx (D1). 13 reine Funktionen ohne
Komponenten-State: getStartOfWeek, getDayName, getTimeSlots, formatHour,
getWeekNumber, formatCustomHeader und weitere.

11.830 -> 11.655 Zeilen. Mit npm run build verifiziert (Exit 0).

Nebenbefund: getWeekNumber existiert im Projekt dreimal -- hier, in
WeekPrintModal.tsx und in api/user/export-week-view/route.ts. Das Ersetzen
der Duplikate steht in TODO.md; vorher gehoeren die drei Implementierungen
auf Gleichheit geprueft.

v1.117.6

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-09-07 09:09:26 +02:00
parent 97cb306853
commit c303e1cf2b
6 changed files with 229 additions and 182 deletions

View File

@ -2,6 +2,19 @@
All notable changes to this project are documented in this file. All notable changes to this project are documented in this file.
## [1.117.6] - 2026-09-07
### Changed
- Datums-, Wochen- und Zeitraster-Helfer nach `src/lib/weekly/date-utils.ts` verschoben
(13 reine Funktionen ohne Komponenten-State: `getStartOfWeek`, `getDayName`,
`getTimeSlots`, `formatHour`, `getWeekNumber`, `formatCustomHeader` und weitere).
`WeeklyView.tsx`: 11.830 → 11.655 Zeilen. Mit `npm run build` verifiziert (Exit 0).
### Notes
- `getWeekNumber` existiert im Projekt **dreimal** — hier, in `WeekPrintModal.tsx` und in
`app/api/user/export-week-view/route.ts`. Die Duplikate zu ersetzen steht in TODO.md;
vorher müssen die drei Implementierungen auf Gleichheit geprüft werden.
## [1.117.5] - 2026-09-07 ## [1.117.5] - 2026-09-07
### Changed ### Changed

View File

@ -128,7 +128,7 @@ Betroffen sind genau zwei Dateien:
| Datei | Zeilen | | Datei | Zeilen |
|---|---:| |---|---:|
| `src/components/WeeklyView.tsx` | 11.830 *(war 11.906)* | | `src/components/WeeklyView.tsx` | 11.655 *(war 11.906)* |
| `src/components/SettingsSidebar.tsx` | 5.445 | | `src/components/SettingsSidebar.tsx` | 5.445 |
**Voraussetzung:** C1 muss laufen — ohne Typecheck ist ein Refactoring dieser Größe **Voraussetzung:** C1 muss laufen — ohne Typecheck ist ein Refactoring dieser Größe
@ -137,7 +137,12 @@ Blindflug. Ebenso hilfreich: A2, damit es überhaupt Tests gibt.
#### WeeklyView.tsx → Zielstruktur #### WeeklyView.tsx → Zielstruktur
- [x] Typen nach `src/lib/weekly/types.ts` (7 Typen, 97 Zeilen) — Build verifiziert - [x] Typen nach `src/lib/weekly/types.ts` (7 Typen, 97 Zeilen) — Build verifiziert
- [ ] `src/components/weekly/` anlegen - [ ] `src/components/weekly/` anlegen
- [ ] Reine Helfer nach `src/lib/weekly/` (Datum, Slots, Formatierung) - [x] Datums-/Zeitraster-Helfer nach `src/lib/weekly/date-utils.ts` (13 Funktionen, 202 Zeilen)
- [ ] Cookie-/Geräteeinstellungen nach `src/lib/weekly/device-settings.ts`
- [ ] Formatierungs-Helfer (`stripHtml`, `invertColor`, `isAllDayEvent`) auslagern
- [ ] `getWeekNumber` existiert **dreimal** im Projekt (WeeklyView, `WeekPrintModal.tsx`,
`api/user/export-week-view/route.ts`) — Duplikate gegen `date-utils.ts` ersetzen,
vorher die drei Implementierungen auf Gleichheit prüfen
- [ ] Drag & Drop in einen Hook `useTaskDragDrop.ts` - [ ] Drag & Drop in einen Hook `useTaskDragDrop.ts`
- [ ] Someday-Listen in `SomedayLists.tsx` - [ ] Someday-Listen in `SomedayLists.tsx`
- [ ] All-Day-Leiste in `AllDayBar.tsx` - [ ] All-Day-Leiste in `AllDayBar.tsx`

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "my-weekly-todo-list", "name": "my-weekly-todo-list",
"version": "1.117.5", "version": "1.117.6",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "my-weekly-todo-list", "name": "my-weekly-todo-list",
"version": "1.117.5", "version": "1.117.6",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@auth/prisma-adapter": "^2.11.1", "@auth/prisma-adapter": "^2.11.1",

View File

@ -1,6 +1,6 @@
{ {
"name": "my-weekly-todo-list", "name": "my-weekly-todo-list",
"version": "1.117.5", "version": "1.117.6",
"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": {

View File

@ -213,195 +213,23 @@ const useGoogleFonts = (fonts: string[]) => {
}, [fonts]); }, [fonts]);
}; };
// Date utilities import {
function getStartOfWeek(date: Date, startDay: number = 0): Date { getStartOfWeek, formatDateHeader, getDayName, isSameDay, formatDateToISO,
const d = new Date(date); parseCalendarDate, formatHour, getTimeSlots, getHourFromSlot, getWeekNumber,
const day = d.getDay(); getCWReferenceDate, formatCustomHeader, getSelectedDay,
const diff = d.getDate() - day + (day < startDay ? -7 : 0) + startDay; // if today is sun(0) and start is mon(1), day < start (0 < 1) -> -7 + 1 = -6. 0 - 6 = -6. Correct. } from "@/lib/weekly/date-utils";
// Wait, let's re-verify:
// Start Mon(1). Today Sun(0). day=0. diff = date - 0 + (-7) + 1 = date - 6. Correct (last Monday).
// Start Mon(1). Today Mon(1). day=1. diff = date - 1 + (0) + 1 = date. Correct.
// Start Sun(0). Today Mon(1). day=1. diff = date - 1 + (0) + 0 = date - 1. Correct (last Sunday).
// Start Sun(0). Today Sun(0). day=0. diff = date - 0 + (0) + 0 = date. Correct.
// What if Start Mon(1), Today Tue(2). day=2. diff = date - 2 + 0 + 1 = date - 1. Correct.
// Better logic:
// const day = d.getDay();
// const diff = (day < startDay ? 7 : 0) + day - startDay;
// d.setDate(d.getDate() - diff);
//
// Let's stick to a robust one:
const currentDay = d.getDay();
const distance = (currentDay - startDay + 7) % 7;
d.setDate(d.getDate() - distance);
return d;
}
function formatDateHeader(date: Date, locale: string = "en-US"): string {
return date.toLocaleDateString(locale, { day: "numeric", month: "short" }); // e.g. 12. Feb.
}
function getDayName(date: Date, locale: string = "en-US", format?: string, customNames?: string, weekStartDay: number = 0, dayCase: string = "capitalize"): string {
let name: string;
if (format === "custom" && customNames) {
// Split by comma or semicolon to allow spaces in names
const names = customNames.split(/[,;]+/).map(s => s.trim()).filter(Boolean);
if (names.length === 7) {
// Adjust index based on weekStartDay (0=Sun, 1=Mon)
const index = (date.getDay() - weekStartDay + 7) % 7;
name = names[index];
} else {
name = date.toLocaleDateString(locale, { weekday: "long" });
}
} else {
const weekdayOption = format === "narrow" ? "narrow" : (format === "short" ? "short" : "long");
try {
name = date.toLocaleDateString(locale, { weekday: weekdayOption });
} catch (e) {
name = date.toLocaleDateString("en-US", { weekday: weekdayOption });
}
}
if (dayCase === "uppercase") return name.toUpperCase();
if (dayCase === "normal") return name.toLowerCase();
// capitalize: first letter uppercase, rest lowercase
return name.charAt(0).toUpperCase() + name.slice(1).toLowerCase();
}
function isSameDay(d1: Date, d2: Date): boolean {
return d1.toDateString() === d2.toDateString();
}
function formatDateToISO(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}`;
}
/**
* Parses a date string from a calendar event.
* If strictly a date (YYYY-MM-DD), it's parsed as local mid-night.
* If an ISO string with time, it's parsed regularly.
*/
function parseCalendarDate(dateStr: string): Date {
if (!dateStr) return new Date();
// If it's date-only (YYYY-MM-DD), parse as local midnight
if (!dateStr.includes("T")) {
const parts = dateStr.split("-").map(Number);
if (parts.length === 3) {
return new Date(parts[0], parts[1] - 1, parts[2], 0, 0, 0);
}
}
// If it's an ISO string but we want local midnight (e.g. from cache or older backend)
// we still parse it. The fix in the backend should reduce this.
return new Date(dateStr);
}
function formatHour(hour: number, minutes: number = 0, format: "short" | "full" = "short", timeFormat: string = "24h"): string {
if (timeFormat === "12h") {
const h = hour % 12 || 12;
const ampm = hour >= 12 ? "PM" : "AM";
const m = minutes > 0 ? `:${minutes.toString().padStart(2, "0")}` : "";
return format === "full" || minutes > 0 ? `${h}:${minutes.toString().padStart(2, "0")} ${ampm}` : `${h}${m} ${ampm}`;
}
// 24h format
if (format === "short" && minutes === 0) {
return `${hour}`;
}
return `${hour.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`;
}
function getTimeSlots(
cellDuration: CellDuration,
startHour: number,
endHour: number,
): string[] {
const slots: string[] = [];
const startMins = startHour * 60;
const endMins = endHour * 60;
for (let mins = startMins; mins < endMins; mins += cellDuration) {
const h = Math.floor(mins / 60);
const m = mins % 60;
slots.push(`${h.toString().padStart(2, "0")}:${m.toString().padStart(2, "0")}`);
}
return slots;
}
function getHourFromSlot(slot: string): number {
return parseInt(slot.split(":")[0], 10);
}
function getWeekNumber(date: Date): number {
// ISO 8601 week number: weeks start on Monday
const d = new Date(
Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()),
);
const dayNum = d.getUTCDay() || 7;
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
return Math.ceil(((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
}
// Get the Monday of the ISO week that the first visible day belongs to.
// This ensures CW changes when the first visible day crosses into a new ISO week.
function getCWReferenceDate(days: Date[]): Date {
if (days.length === 0) return new Date();
const first = days[0];
const dow = first.getDay(); // 0=Sun, 1=Mon, ..., 6=Sat
// Calculate distance back to Monday (ISO week start)
// Sunday (0) → go back 6 days to previous Monday
// Monday (1) → 0, Tuesday (2) → 1, etc.
const distToMonday = dow === 0 ? 6 : dow - 1;
return new Date(first.getTime() - distToMonday * 86400000);
}
// Format a custom header string using tokens
function formatCustomHeader(format: string, days: Date[], language: string, t: any, refDateOverride?: Date): string {
if (!format) return "";
// Choose the reference date: if today is within the visible days, use today.
// Otherwise, use the standard CW reference date (start of week).
const today = new Date();
const isTodayInWeek = days.some(d =>
d.getDate() === today.getDate() &&
d.getMonth() === today.getMonth() &&
d.getFullYear() === today.getFullYear()
);
const refDate = refDateOverride ?? (isTodayInWeek ? today : getCWReferenceDate(days));
// Define token mappings
const tokens: Record<string, string> = {
"YYYY": refDate.getFullYear().toString(),
"WW": getWeekNumber(refDate).toString().padStart(2, '0'),
"MMMM": refDate.toLocaleDateString(language, { month: 'long' }),
"MMM": refDate.toLocaleDateString(language, { month: 'short' }),
"MM": (refDate.getMonth() + 1).toString().padStart(2, '0'),
"M": (refDate.getMonth() + 1).toString(),
"DDDD": refDate.toLocaleDateString(language, { weekday: 'long' }),
"DDD": refDate.toLocaleDateString(language, { weekday: 'short' }),
"DD": refDate.getDate().toString().padStart(2, '0'),
"D": refDate.getDate().toString(),
"[TODAY]": today.toLocaleDateString(language, { day: '2-digit', month: '2-digit', year: 'numeric' })
};
// Single-pass replacement using regex to avoid nested replacements (e.g. M in MMMM)
// Standalone 'W' removed to allow literal 'W' (like in 'KW')
const regex = /\[TODAY\]|YYYY|WW|MMMM|MMM|MM|M|DDDD|DDD|DD|D/g;
return format.replace(regex, (match) => tokens[match] || match);
}
// Get the "selected" day for current_day header.
// Priority: explicit selection → today if visible → first visible day
function getSelectedDay(days: Date[], explicitSelection?: Date | null): Date {
if (explicitSelection) return explicitSelection;
const today = new Date();
return days.some(d =>
d.getDate() === today.getDate() &&
d.getMonth() === today.getMonth() &&
d.getFullYear() === today.getFullYear()
) ? today : days[0];
}
// Check if an event is an all-day event // Check if an event is an all-day event
// Defined outside component to avoid stale closure issues in useCallbacks // Defined outside component to avoid stale closure issues in useCallbacks

View File

@ -0,0 +1,201 @@
import type { CellDuration } from "@/lib/weekly/types";
/**
* Datums-, Wochen- und Zeitraster-Helfer der Wochenansicht.
*
* Reine Funktionen ohne Komponenten-State — lagen bis v1.117.6 in WeeklyView.tsx.
*
* Hinweis: getWeekNumber existiert im Projekt noch zweimal separat, in
* WeekPrintModal.tsx und in app/api/user/export-week-view/route.ts. Diese Duplikate
* gegen diese Fassung zu ersetzen ist ein eigener Schritt (siehe TODO.md D1).
*/
// Date utilities
export function getStartOfWeek(date: Date, startDay: number = 0): Date {
const d = new Date(date);
const day = d.getDay();
const diff = d.getDate() - day + (day < startDay ? -7 : 0) + startDay; // if today is sun(0) and start is mon(1), day < start (0 < 1) -> -7 + 1 = -6. 0 - 6 = -6. Correct.
// Wait, let's re-verify:
// Start Mon(1). Today Sun(0). day=0. diff = date - 0 + (-7) + 1 = date - 6. Correct (last Monday).
// Start Mon(1). Today Mon(1). day=1. diff = date - 1 + (0) + 1 = date. Correct.
// Start Sun(0). Today Mon(1). day=1. diff = date - 1 + (0) + 0 = date - 1. Correct (last Sunday).
// Start Sun(0). Today Sun(0). day=0. diff = date - 0 + (0) + 0 = date. Correct.
// What if Start Mon(1), Today Tue(2). day=2. diff = date - 2 + 0 + 1 = date - 1. Correct.
// Better logic:
// const day = d.getDay();
// const diff = (day < startDay ? 7 : 0) + day - startDay;
// d.setDate(d.getDate() - diff);
//
// Let's stick to a robust one:
const currentDay = d.getDay();
const distance = (currentDay - startDay + 7) % 7;
d.setDate(d.getDate() - distance);
return d;
}
export function formatDateHeader(date: Date, locale: string = "en-US"): string {
return date.toLocaleDateString(locale, { day: "numeric", month: "short" }); // e.g. 12. Feb.
}
export function getDayName(date: Date, locale: string = "en-US", format?: string, customNames?: string, weekStartDay: number = 0, dayCase: string = "capitalize"): string {
let name: string;
if (format === "custom" && customNames) {
// Split by comma or semicolon to allow spaces in names
const names = customNames.split(/[,;]+/).map(s => s.trim()).filter(Boolean);
if (names.length === 7) {
// Adjust index based on weekStartDay (0=Sun, 1=Mon)
const index = (date.getDay() - weekStartDay + 7) % 7;
name = names[index];
} else {
name = date.toLocaleDateString(locale, { weekday: "long" });
}
} else {
const weekdayOption = format === "narrow" ? "narrow" : (format === "short" ? "short" : "long");
try {
name = date.toLocaleDateString(locale, { weekday: weekdayOption });
} catch (e) {
name = date.toLocaleDateString("en-US", { weekday: weekdayOption });
}
}
if (dayCase === "uppercase") return name.toUpperCase();
if (dayCase === "normal") return name.toLowerCase();
// capitalize: first letter uppercase, rest lowercase
return name.charAt(0).toUpperCase() + name.slice(1).toLowerCase();
}
export function isSameDay(d1: Date, d2: Date): boolean {
return d1.toDateString() === d2.toDateString();
}
export function formatDateToISO(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}`;
}
/**
* Parses a date string from a calendar event.
* If strictly a date (YYYY-MM-DD), it's parsed as local mid-night.
* If an ISO string with time, it's parsed regularly.
*/
export function parseCalendarDate(dateStr: string): Date {
if (!dateStr) return new Date();
// If it's date-only (YYYY-MM-DD), parse as local midnight
if (!dateStr.includes("T")) {
const parts = dateStr.split("-").map(Number);
if (parts.length === 3) {
return new Date(parts[0], parts[1] - 1, parts[2], 0, 0, 0);
}
}
// If it's an ISO string but we want local midnight (e.g. from cache or older backend)
// we still parse it. The fix in the backend should reduce this.
return new Date(dateStr);
}
export function formatHour(hour: number, minutes: number = 0, format: "short" | "full" = "short", timeFormat: string = "24h"): string {
if (timeFormat === "12h") {
const h = hour % 12 || 12;
const ampm = hour >= 12 ? "PM" : "AM";
const m = minutes > 0 ? `:${minutes.toString().padStart(2, "0")}` : "";
return format === "full" || minutes > 0 ? `${h}:${minutes.toString().padStart(2, "0")} ${ampm}` : `${h}${m} ${ampm}`;
}
// 24h format
if (format === "short" && minutes === 0) {
return `${hour}`;
}
return `${hour.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`;
}
export function getTimeSlots(
cellDuration: CellDuration,
startHour: number,
endHour: number,
): string[] {
const slots: string[] = [];
const startMins = startHour * 60;
const endMins = endHour * 60;
for (let mins = startMins; mins < endMins; mins += cellDuration) {
const h = Math.floor(mins / 60);
const m = mins % 60;
slots.push(`${h.toString().padStart(2, "0")}:${m.toString().padStart(2, "0")}`);
}
return slots;
}
export function getHourFromSlot(slot: string): number {
return parseInt(slot.split(":")[0], 10);
}
export function getWeekNumber(date: Date): number {
// ISO 8601 week number: weeks start on Monday
const d = new Date(
Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()),
);
const dayNum = d.getUTCDay() || 7;
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
return Math.ceil(((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
}
// Get the Monday of the ISO week that the first visible day belongs to.
// This ensures CW changes when the first visible day crosses into a new ISO week.
export function getCWReferenceDate(days: Date[]): Date {
if (days.length === 0) return new Date();
const first = days[0];
const dow = first.getDay(); // 0=Sun, 1=Mon, ..., 6=Sat
// Calculate distance back to Monday (ISO week start)
// Sunday (0) → go back 6 days to previous Monday
// Monday (1) → 0, Tuesday (2) → 1, etc.
const distToMonday = dow === 0 ? 6 : dow - 1;
return new Date(first.getTime() - distToMonday * 86400000);
}
// Format a custom header string using tokens
export function formatCustomHeader(format: string, days: Date[], language: string, t: any, refDateOverride?: Date): string {
if (!format) return "";
// Choose the reference date: if today is within the visible days, use today.
// Otherwise, use the standard CW reference date (start of week).
const today = new Date();
const isTodayInWeek = days.some(d =>
d.getDate() === today.getDate() &&
d.getMonth() === today.getMonth() &&
d.getFullYear() === today.getFullYear()
);
const refDate = refDateOverride ?? (isTodayInWeek ? today : getCWReferenceDate(days));
// Define token mappings
const tokens: Record<string, string> = {
"YYYY": refDate.getFullYear().toString(),
"WW": getWeekNumber(refDate).toString().padStart(2, '0'),
"MMMM": refDate.toLocaleDateString(language, { month: 'long' }),
"MMM": refDate.toLocaleDateString(language, { month: 'short' }),
"MM": (refDate.getMonth() + 1).toString().padStart(2, '0'),
"M": (refDate.getMonth() + 1).toString(),
"DDDD": refDate.toLocaleDateString(language, { weekday: 'long' }),
"DDD": refDate.toLocaleDateString(language, { weekday: 'short' }),
"DD": refDate.getDate().toString().padStart(2, '0'),
"D": refDate.getDate().toString(),
"[TODAY]": today.toLocaleDateString(language, { day: '2-digit', month: '2-digit', year: 'numeric' })
};
// Single-pass replacement using regex to avoid nested replacements (e.g. M in MMMM)
// Standalone 'W' removed to allow literal 'W' (like in 'KW')
const regex = /\[TODAY\]|YYYY|WW|MMMM|MMM|MM|M|DDDD|DDD|DD|D/g;
return format.replace(regex, (match) => tokens[match] || match);
}
// Get the "selected" day for current_day header.
// Priority: explicit selection → today if visible → first visible day
export function getSelectedDay(days: Date[], explicitSelection?: Date | null): Date {
if (explicitSelection) return explicitSelection;
const today = new Date();
return days.some(d =>
d.getDate() === today.getDate() &&
d.getMonth() === today.getMonth() &&
d.getFullYear() === today.getFullYear()
) ? today : days[0];
}