refactor: Datums- und Zeitraster-Helfer nach src/lib/weekly/date-utils.ts
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:
parent
97cb306853
commit
c303e1cf2b
13
CHANGELOG.md
13
CHANGELOG.md
@ -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
|
||||||
|
|||||||
9
TODO.md
9
TODO.md
@ -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
4
package-lock.json
generated
@ -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",
|
||||||
|
|||||||
@ -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": {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
201
src/lib/weekly/date-utils.ts
Normal file
201
src/lib/weekly/date-utils.ts
Normal 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];
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user