Refactors date handling for ISO week compatibility

Centralizes all date calculations into a new `DateCalculator` class for better maintainability and consistency.

Ensures correct ISO week handling (Monday as the first day) throughout the calendar.

Updates `CalendarConfig` to use ISO day numbering (1-7 for Mon-Sun) for work week definitions.

Fixes issue where date calculations were inconsistent.
Enhances event rendering and navigation.
Updates navigation logic to use pre-rendered events.
Removes the need for `CONTAINER_READY_FOR_EVENTS` event.
This commit is contained in:
Janus Knudsen 2025-08-20 00:39:31 +02:00
parent efc1742dad
commit 7d513600d8
13 changed files with 230 additions and 343 deletions

View file

@ -13,7 +13,7 @@ export class DateCalculator {
}
/**
* Get dates for work week based on ISO week (starts Monday)
* Get dates for work week using ISO 8601 day numbering (Monday=1, Sunday=7)
* @param weekStart - Any date in the week
* @returns Array of dates for the configured work days
*/
@ -21,21 +21,15 @@ export class DateCalculator {
const dates: Date[] = [];
const workWeekSettings = this.config.getWorkWeekSettings();
// Get Monday of the ISO week
const monday = this.getISOWeekStart(weekStart);
// Always use ISO week start (Monday)
const mondayOfWeek = this.getISOWeekStart(weekStart);
// Calculate dates for each work day
workWeekSettings.workDays.forEach(dayOfWeek => {
const date = new Date(monday);
if (dayOfWeek === 0) {
// Sunday is 6 days after Monday
date.setDate(monday.getDate() + 6);
} else {
// Monday=1 becomes 0 days after, Tuesday=2 becomes 1 day after, etc.
date.setDate(monday.getDate() + dayOfWeek - 1);
}
// Calculate dates for each work day using ISO numbering
workWeekSettings.workDays.forEach(isoDay => {
const date = new Date(mondayOfWeek);
// ISO day 1=Monday is +0 days, ISO day 7=Sunday is +6 days
const daysFromMonday = isoDay === 7 ? 6 : isoDay - 1;
date.setDate(mondayOfWeek.getDate() + daysFromMonday);
dates.push(date);
});
@ -56,24 +50,14 @@ export class DateCalculator {
return monday;
}
/**
* Get the start of the week for a given date (legacy method)
* @param date - Any date in the week
* @param firstDayOfWeek - 0 for Sunday, 1 for Monday
* @returns The start date of the week
*/
getWeekStart(date: Date, firstDayOfWeek: number = 1): Date {
return this.getISOWeekStart(date);
}
/**
* Get the end of the week for a given date
* Get the end of the ISO week for a given date
* @param date - Any date in the week
* @param firstDayOfWeek - 0 for Sunday, 1 for Monday
* @returns The end date of the week
* @returns The end date of the ISO week (Sunday)
*/
getWeekEnd(date: Date, firstDayOfWeek: number = 1): Date {
const weekStart = this.getWeekStart(date, firstDayOfWeek);
getWeekEnd(date: Date): Date {
const weekStart = this.getISOWeekStart(date);
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekStart.getDate() + 6);
weekEnd.setHours(23, 59, 59, 999);
@ -94,28 +78,40 @@ export class DateCalculator {
}
/**
* Format a date range for display
* Format a date range with customizable options
* @param start - Start date
* @param end - End date
* @param options - Formatting options
* @returns Formatted date range string
*/
formatDateRange(start: Date, end: Date): string {
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
formatDateRange(
start: Date,
end: Date,
options: {
locale?: string;
month?: 'numeric' | '2-digit' | 'long' | 'short' | 'narrow';
day?: 'numeric' | '2-digit';
year?: 'numeric' | '2-digit';
} = {}
): string {
const { locale = 'en-US', month = 'short', day = 'numeric' } = options;
const startMonth = months[start.getMonth()];
const endMonth = months[end.getMonth()];
const startDay = start.getDate();
const endDay = end.getDate();
const startYear = start.getFullYear();
const endYear = end.getFullYear();
if (startYear !== endYear) {
return `${startMonth} ${startDay}, ${startYear} - ${endMonth} ${endDay}, ${endYear}`;
} else if (startMonth === endMonth) {
return `${startMonth} ${startDay} - ${endDay}, ${startYear}`;
} else {
return `${startMonth} ${startDay} - ${endMonth} ${endDay}, ${startYear}`;
const formatter = new Intl.DateTimeFormat(locale, {
month,
day,
year: startYear !== endYear ? 'numeric' : undefined
});
// @ts-ignore
if (typeof formatter.formatRange === 'function') {
// @ts-ignore
return formatter.formatRange(start, end);
}
return `${formatter.format(start)} - ${formatter.format(end)}`;
}
/**
@ -173,18 +169,110 @@ export class DateCalculator {
}
/**
* Get the day name for a date
* Get the day name for a date using Intl.DateTimeFormat
* @param date - Date to get day name for
* @param format - 'short' or 'long'
* @returns Day name
*/
getDayName(date: Date, format: 'short' | 'long' = 'short'): string {
const days = {
short: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
long: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
};
return days[format][date.getDay()];
const formatter = new Intl.DateTimeFormat('en-US', {
weekday: format
});
return formatter.format(date);
}
/**
* Format time to HH:MM
* @param date - Date to format
* @returns Time string
*/
formatTime(date: Date): string {
return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
}
/**
* Format time to 12-hour format
* @param date - Date to format
* @returns 12-hour time string
*/
formatTime12(date: Date): string {
const hours = date.getHours();
const minutes = date.getMinutes();
const period = hours >= 12 ? 'PM' : 'AM';
const displayHours = hours % 12 || 12;
return `${displayHours}:${String(minutes).padStart(2, '0')} ${period}`;
}
/**
* Convert minutes since midnight to time string
* @param minutes - Minutes since midnight
* @returns Time string
*/
minutesToTime(minutes: number): string {
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
const period = hours >= 12 ? 'PM' : 'AM';
const displayHours = hours % 12 || 12;
return `${displayHours}:${String(mins).padStart(2, '0')} ${period}`;
}
/**
* Convert time string to minutes since midnight
* @param timeStr - Time string
* @returns Minutes since midnight
*/
timeToMinutes(timeStr: string): number {
const [time] = timeStr.split('T').pop()!.split('.');
const [hours, minutes] = time.split(':').map(Number);
return hours * 60 + minutes;
}
/**
* Get minutes since start of day
* @param date - Date or ISO string
* @returns Minutes since midnight
*/
getMinutesSinceMidnight(date: Date | string): number {
const d = typeof date === 'string' ? new Date(date) : date;
return d.getHours() * 60 + d.getMinutes();
}
/**
* Calculate duration in minutes between two dates
* @param start - Start date or ISO string
* @param end - End date or ISO string
* @returns Duration in minutes
*/
getDurationMinutes(start: Date | string, end: Date | string): number {
const startDate = typeof start === 'string' ? new Date(start) : start;
const endDate = typeof end === 'string' ? new Date(end) : end;
return Math.floor((endDate.getTime() - startDate.getTime()) / 60000);
}
/**
* Check if two dates are on the same day
* @param date1 - First date
* @param date2 - Second date
* @returns True if same day
*/
isSameDay(date1: Date, date2: Date): boolean {
return date1.toDateString() === date2.toDateString();
}
/**
* Check if event spans multiple days
* @param start - Start date or ISO string
* @param end - End date or ISO string
* @returns True if spans multiple days
*/
isMultiDay(start: Date | string, end: Date | string): boolean {
const startDate = typeof start === 'string' ? new Date(start) : start;
const endDate = typeof end === 'string' ? new Date(end) : end;
return !this.isSameDay(startDate, endDate);
}
}
// Create singleton instance with config

View file

@ -1,230 +0,0 @@
// Date and time utility functions
/**
* Date and time utility functions
*/
export class DateUtils {
/**
* Get start of week for a given date
*/
static getWeekStart(date: Date, firstDayOfWeek: number = 1): Date {
const d = new Date(date);
const day = d.getDay();
const diff = (day - firstDayOfWeek + 7) % 7;
d.setDate(d.getDate() - diff);
d.setHours(0, 0, 0, 0);
return d;
}
/**
* Get end of week for a given date
*/
static getWeekEnd(date: Date, firstDayOfWeek: number = 1): Date {
const start = this.getWeekStart(date, firstDayOfWeek);
const end = new Date(start);
end.setDate(end.getDate() + 6);
end.setHours(23, 59, 59, 999);
return end;
}
/**
* Format date to YYYY-MM-DD
*/
static formatDate(date: Date): string {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
}
/**
* Format time to HH:MM
*/
static formatTime(date: Date): string {
return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
}
/**
* Format time to 12-hour format
*/
static formatTime12(date: Date): string {
const hours = date.getHours();
const minutes = date.getMinutes();
const period = hours >= 12 ? 'PM' : 'AM';
const displayHours = hours % 12 || 12;
return `${displayHours}:${String(minutes).padStart(2, '0')} ${period}`;
}
/**
* Convert minutes since midnight to time string
*/
static minutesToTime(minutes: number): string {
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
const period = hours >= 12 ? 'PM' : 'AM';
const displayHours = hours % 12 || 12;
return `${displayHours}:${String(mins).padStart(2, '0')} ${period}`;
}
/**
* Convert time string to minutes since midnight
*/
static timeToMinutes(timeStr: string): number {
const [time] = timeStr.split('T').pop()!.split('.');
const [hours, minutes] = time.split(':').map(Number);
return hours * 60 + minutes;
}
/**
* Get minutes since start of day
*/
static getMinutesSinceMidnight(date: Date | string): number {
const d = typeof date === 'string' ? new Date(date) : date;
return d.getHours() * 60 + d.getMinutes();
}
/**
* Calculate duration in minutes between two dates
*/
static getDurationMinutes(start: Date | string, end: Date | string): number {
const startDate = typeof start === 'string' ? new Date(start) : start;
const endDate = typeof end === 'string' ? new Date(end) : end;
return Math.floor((endDate.getTime() - startDate.getTime()) / 60000);
}
/**
* Check if date is today
*/
static isToday(date: Date): boolean {
const today = new Date();
return date.toDateString() === today.toDateString();
}
/**
* Check if two dates are on the same day
*/
static isSameDay(date1: Date, date2: Date): boolean {
return date1.toDateString() === date2.toDateString();
}
/**
* Check if event spans multiple days
*/
static isMultiDay(start: Date | string, end: Date | string): boolean {
const startDate = typeof start === 'string' ? new Date(start) : start;
const endDate = typeof end === 'string' ? new Date(end) : end;
return !this.isSameDay(startDate, endDate);
}
/**
* Get day name
*/
static getDayName(date: Date, format: 'short' | 'long' = 'short'): string {
const days = {
short: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
long: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
};
return days[format][date.getDay()];
}
/**
* Add days to date
*/
static addDays(date: Date, days: number): Date {
const result = new Date(date);
result.setDate(result.getDate() + days);
return result;
}
/**
* Add minutes to date
*/
static addMinutes(date: Date, minutes: number): Date {
const result = new Date(date);
result.setMinutes(result.getMinutes() + minutes);
return result;
}
/**
* Snap time to nearest interval
*/
static snapToInterval(date: Date, intervalMinutes: number): Date {
const minutes = date.getMinutes();
const snappedMinutes = Math.round(minutes / intervalMinutes) * intervalMinutes;
const result = new Date(date);
result.setMinutes(snappedMinutes);
result.setSeconds(0);
result.setMilliseconds(0);
return result;
}
/**
* Get current time in minutes since day start
*/
static getCurrentTimeMinutes(dayStartHour: number = 0): number {
const now = new Date();
const minutesSinceMidnight = now.getHours() * 60 + now.getMinutes();
return minutesSinceMidnight - (dayStartHour * 60);
}
/**
* Format duration to human readable string
*/
static formatDuration(minutes: number): string {
if (minutes < 60) {
return `${minutes} min`;
}
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
if (mins === 0) {
return `${hours} hour${hours > 1 ? 's' : ''}`;
}
return `${hours} hour${hours > 1 ? 's' : ''} ${mins} min`;
}
/**
* Get ISO week number for a given date
*/
static getWeekNumber(date: Date): number {
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 month names array
*/
static getMonthNames(format: 'short' | 'long' = 'short'): string[] {
const months = {
short: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
long: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
};
return months[format];
}
/**
* Format date range for display (e.g., "Jan 15 - 21, 2024" or "Jan 15 - Feb 2, 2024")
*/
static formatDateRange(startDate: Date, endDate: Date): string {
const monthNames = this.getMonthNames('short');
const startMonth = monthNames[startDate.getMonth()];
const endMonth = monthNames[endDate.getMonth()];
const startDay = startDate.getDate();
const endDay = endDate.getDate();
const startYear = startDate.getFullYear();
const endYear = endDate.getFullYear();
if (startMonth === endMonth && startYear === endYear) {
return `${startMonth} ${startDay} - ${endDay}, ${startYear}`;
} else if (startYear !== endYear) {
return `${startMonth} ${startDay}, ${startYear} - ${endMonth} ${endDay}, ${endYear}`;
} else {
return `${startMonth} ${startDay} - ${endMonth} ${endDay}, ${startYear}`;
}
}
}