diff --git a/src/elements/SwpEventElement.ts b/src/elements/SwpEventElement.ts index 60070de..e357b62 100644 --- a/src/elements/SwpEventElement.ts +++ b/src/elements/SwpEventElement.ts @@ -244,7 +244,7 @@ export class SwpAllDayEventElement extends BaseEventElement { * Set all-day specific attributes */ private setAllDayAttributes(): void { - this.element.dataset.allDay = "true"; + this.element.dataset.allday = "true"; this.element.dataset.start = this.event.start.toISOString(); this.element.dataset.end = this.event.end.toISOString(); } diff --git a/src/index.ts b/src/index.ts index f9c0049..ffb3cd8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,6 @@ import { eventBus } from './core/EventBus'; import { calendarConfig } from './core/CalendarConfig'; import { CalendarTypeFactory } from './factories/CalendarTypeFactory'; import { ManagerFactory } from './factories/ManagerFactory'; -import { DateCalculator } from './utils/DateCalculator'; import { URLManager } from './utils/URLManager'; import { CalendarManagers } from './types/ManagerTypes'; @@ -40,9 +39,6 @@ async function initializeCalendar(): Promise { // Use the singleton calendar configuration const config = calendarConfig; - // Initialize DateCalculator with config first - DateCalculator.initialize(config); - // Initialize the CalendarTypeFactory before creating managers CalendarTypeFactory.initialize(); diff --git a/src/managers/CalendarManager.ts b/src/managers/CalendarManager.ts index e09b0df..53f5c72 100644 --- a/src/managers/CalendarManager.ts +++ b/src/managers/CalendarManager.ts @@ -7,7 +7,7 @@ import { GridManager } from './GridManager'; import { HeaderManager } from './HeaderManager'; import { EventRenderingService } from '../renderers/EventRendererManager'; import { ScrollManager } from './ScrollManager'; -import { DateCalculator } from '../utils/DateCalculator'; +import { DateService } from '../utils/DateService'; import { EventFilterManager } from './EventFilterManager'; import { InitializationReport } from '../types/ManagerTypes'; @@ -23,7 +23,7 @@ export class CalendarManager { private eventRenderer: EventRenderingService; private scrollManager: ScrollManager; private eventFilterManager: EventFilterManager; - private dateCalculator: DateCalculator; + private dateService: DateService; private currentView: CalendarView = 'week'; private currentDate: Date = new Date(); private isInitialized: boolean = false; @@ -42,8 +42,8 @@ export class CalendarManager { this.eventRenderer = eventRenderer; this.scrollManager = scrollManager; this.eventFilterManager = new EventFilterManager(); - DateCalculator.initialize(calendarConfig); - this.dateCalculator = new DateCalculator(); + const timezone = calendarConfig.getTimezone?.() || 'Europe/Copenhagen'; + this.dateService = new DateService(timezone); this.setupEventListeners(); } @@ -451,10 +451,10 @@ export class CalendarManager { const lastDate = new Date(lastDateStr); // Calculate week number from first date - const weekNumber = DateCalculator.getWeekNumber(firstDate); + const weekNumber = this.dateService.getWeekNumber(firstDate); // Format date range - const dateRange = DateCalculator.formatDateRange(firstDate, lastDate); + const dateRange = this.dateService.formatDateRange(firstDate, lastDate); // Emit week info update this.eventBus.emit(CoreEvents.PERIOD_INFO_UPDATE, { diff --git a/src/managers/DragDropManager.ts b/src/managers/DragDropManager.ts index b3d9ec3..002f96c 100644 --- a/src/managers/DragDropManager.ts +++ b/src/managers/DragDropManager.ts @@ -42,9 +42,6 @@ export class DragDropManager { private currentColumnBounds: ColumnBounds | null = null; private isDragStarted = false; - // Header tracking state - private isInHeader = false; - // Movement threshold to distinguish click from drag private readonly dragThreshold = 5; // pixels @@ -460,7 +457,6 @@ export class DragDropManager { this.draggedElement = null; this.draggedClone = null; this.isDragStarted = false; - this.isInHeader = false; } /** @@ -492,15 +488,11 @@ export class DragDropManager { const elementAtPosition = document.elementFromPoint(event.clientX, event.clientY); if (!elementAtPosition) return; - // Check if we're in a header area const headerElement = elementAtPosition.closest('swp-day-header, swp-calendar-header'); const isCurrentlyInHeader = !!headerElement; - // Detect header enter - if (!this.isInHeader && isCurrentlyInHeader && this.draggedClone) { - this.isInHeader = true; + if (isCurrentlyInHeader && !this.draggedClone?.hasAttribute("data-allday")) { - // Calculate target date using existing method const targetColumn = ColumnDetectionUtils.getColumnBounds(position); if (targetColumn) { @@ -510,15 +502,14 @@ export class DragDropManager { targetColumn: targetColumn, mousePosition: { x: event.clientX, y: event.clientY }, originalElement: this.draggedElement, - draggedClone: this.draggedClone + draggedClone: this.draggedClone!! }; this.eventBus.emit('drag:mouseenter-header', dragMouseEnterPayload); } } // Detect header leave - if (this.isInHeader && !isCurrentlyInHeader) { - this.isInHeader = false; + if (isCurrentlyInHeader && this.draggedClone?.hasAttribute("data-allday")) { console.log('🚪 DragDropManager: Emitting drag:mouseleave-header'); diff --git a/src/managers/EventManager.ts b/src/managers/EventManager.ts index 42d193c..01c1760 100644 --- a/src/managers/EventManager.ts +++ b/src/managers/EventManager.ts @@ -2,7 +2,7 @@ import { EventBus } from '../core/EventBus'; import { IEventBus, CalendarEvent, ResourceCalendarData } from '../types/CalendarTypes'; import { CoreEvents } from '../constants/CoreEvents'; import { calendarConfig } from '../core/CalendarConfig'; -import { DateCalculator } from '../utils/DateCalculator'; +import { DateService } from '../utils/DateService'; import { ResourceData } from '../types/ManagerTypes'; interface RawEventData { @@ -26,9 +26,12 @@ export class EventManager { private rawData: ResourceCalendarData | RawEventData[] | null = null; private eventCache = new Map(); // Cache for period queries private lastCacheKey: string = ''; + private dateService: DateService; constructor(eventBus: IEventBus) { this.eventBus = eventBus; + const timezone = calendarConfig.getTimezone?.() || 'Europe/Copenhagen'; + this.dateService = new DateService(timezone); } /** @@ -196,11 +199,11 @@ export class EventManager { } /** - * Optimized events for period with caching and DateCalculator + * Optimized events for period with caching and DateService */ public getEventsForPeriod(startDate: Date, endDate: Date): CalendarEvent[] { - // Create cache key using DateCalculator for consistent formatting - const cacheKey = `${DateCalculator.formatISODate(startDate)}_${DateCalculator.formatISODate(endDate)}`; + // Create cache key using DateService for consistent formatting + const cacheKey = `${this.dateService.formatISODate(startDate)}_${this.dateService.formatISODate(endDate)}`; // Return cached result if available if (this.lastCacheKey === cacheKey && this.eventCache.has(cacheKey)) { diff --git a/src/managers/GridManager.ts b/src/managers/GridManager.ts index 7e84cd8..5cf925e 100644 --- a/src/managers/GridManager.ts +++ b/src/managers/GridManager.ts @@ -9,7 +9,7 @@ import { CoreEvents } from '../constants/CoreEvents'; import { ResourceCalendarData, CalendarView } from '../types/CalendarTypes'; import { GridRenderer } from '../renderers/GridRenderer'; import { GridStyleManager } from '../renderers/GridStyleManager'; -import { DateCalculator } from '../utils/DateCalculator'; +import { DateService } from '../utils/DateService'; /** * Simplified GridManager focused on coordination, delegates rendering to GridRenderer @@ -21,18 +21,35 @@ export class GridManager { private currentView: CalendarView = 'week'; private gridRenderer: GridRenderer; private styleManager: GridStyleManager; + private dateService: DateService; constructor() { // Initialize GridRenderer and StyleManager with config this.gridRenderer = new GridRenderer(); this.styleManager = new GridStyleManager(); + this.dateService = new DateService('Europe/Copenhagen'); this.init(); } private init(): void { this.findElements(); this.subscribeToEvents(); - + } + + /** + * Get the start of the ISO week (Monday) for a given date + */ + private getISOWeekStart(date: Date): Date { + const weekBounds = this.dateService.getWeekBounds(date); + return this.dateService.startOfDay(weekBounds.start); + } + + /** + * Get the end of the ISO week (Sunday) for a given date + */ + private getWeekEnd(date: Date): Date { + const weekBounds = this.dateService.getWeekBounds(date); + return this.dateService.endOfDay(weekBounds.end); } private findElements(): void { @@ -91,7 +108,7 @@ export class GridManager { this.resourceData ); - // Calculate period range using DateCalculator + // Calculate period range const periodRange = this.getPeriodRange(); // Get layout config based on current view @@ -110,42 +127,42 @@ export class GridManager { /** - * Get current period label using DateCalculator + * Get current period label */ public getCurrentPeriodLabel(): string { switch (this.currentView) { case 'week': case 'day': - const weekStart = DateCalculator.getISOWeekStart(this.currentDate); - const weekEnd = DateCalculator.getWeekEnd(this.currentDate); - return DateCalculator.formatDateRange(weekStart, weekEnd); + const weekStart = this.getISOWeekStart(this.currentDate); + const weekEnd = this.getWeekEnd(this.currentDate); + return this.dateService.formatDateRange(weekStart, weekEnd); case 'month': return this.currentDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' }); default: - const defaultWeekStart = DateCalculator.getISOWeekStart(this.currentDate); - const defaultWeekEnd = DateCalculator.getWeekEnd(this.currentDate); - return DateCalculator.formatDateRange(defaultWeekStart, defaultWeekEnd); + const defaultWeekStart = this.getISOWeekStart(this.currentDate); + const defaultWeekEnd = this.getWeekEnd(this.currentDate); + return this.dateService.formatDateRange(defaultWeekStart, defaultWeekEnd); } } /** - * Navigate to next period using DateCalculator + * Navigate to next period */ public navigateNext(): void { let nextDate: Date; switch (this.currentView) { case 'week': - nextDate = DateCalculator.addWeeks(this.currentDate, 1); + nextDate = this.dateService.addWeeks(this.currentDate, 1); break; case 'month': nextDate = this.addMonths(this.currentDate, 1); break; case 'day': - nextDate = DateCalculator.addDays(this.currentDate, 1); + nextDate = this.dateService.addDays(this.currentDate, 1); break; default: - nextDate = DateCalculator.addWeeks(this.currentDate, 1); + nextDate = this.dateService.addWeeks(this.currentDate, 1); } this.currentDate = nextDate; @@ -160,23 +177,23 @@ export class GridManager { } /** - * Navigate to previous period using DateCalculator + * Navigate to previous period */ public navigatePrevious(): void { let prevDate: Date; switch (this.currentView) { case 'week': - prevDate = DateCalculator.addWeeks(this.currentDate, -1); + prevDate = this.dateService.addWeeks(this.currentDate, -1); break; case 'month': prevDate = this.addMonths(this.currentDate, -1); break; case 'day': - prevDate = DateCalculator.addDays(this.currentDate, -1); + prevDate = this.dateService.addDays(this.currentDate, -1); break; default: - prevDate = DateCalculator.addWeeks(this.currentDate, -1); + prevDate = this.dateService.addWeeks(this.currentDate, -1); } this.currentDate = prevDate; @@ -205,20 +222,20 @@ export class GridManager { } /** - * Get current view's display dates using DateCalculator + * Get current view's display dates */ public getDisplayDates(): Date[] { switch (this.currentView) { case 'week': - const weekStart = DateCalculator.getISOWeekStart(this.currentDate); - return DateCalculator.getFullWeekDates(weekStart); + const weekStart = this.getISOWeekStart(this.currentDate); + return this.dateService.getFullWeekDates(weekStart); case 'month': return this.getMonthDates(this.currentDate); case 'day': return [this.currentDate]; default: - const defaultWeekStart = DateCalculator.getISOWeekStart(this.currentDate); - return DateCalculator.getFullWeekDates(defaultWeekStart); + const defaultWeekStart = this.getISOWeekStart(this.currentDate); + return this.dateService.getFullWeekDates(defaultWeekStart); } } @@ -228,8 +245,8 @@ export class GridManager { private getPeriodRange(): { startDate: Date; endDate: Date } { switch (this.currentView) { case 'week': - const weekStart = DateCalculator.getISOWeekStart(this.currentDate); - const weekEnd = DateCalculator.getWeekEnd(this.currentDate); + const weekStart = this.getISOWeekStart(this.currentDate); + const weekEnd = this.getWeekEnd(this.currentDate); return { startDate: weekStart, endDate: weekEnd @@ -245,8 +262,8 @@ export class GridManager { endDate: this.currentDate }; default: - const defaultWeekStart = DateCalculator.getISOWeekStart(this.currentDate); - const defaultWeekEnd = DateCalculator.getWeekEnd(this.currentDate); + const defaultWeekStart = this.getISOWeekStart(this.currentDate); + const defaultWeekEnd = this.getWeekEnd(this.currentDate); return { startDate: defaultWeekStart, endDate: defaultWeekEnd diff --git a/src/managers/NavigationManager.ts b/src/managers/NavigationManager.ts index f0d49b0..e4f46c3 100644 --- a/src/managers/NavigationManager.ts +++ b/src/managers/NavigationManager.ts @@ -1,6 +1,6 @@ import { IEventBus } from '../types/CalendarTypes'; import { EventRenderingService } from '../renderers/EventRendererManager'; -import { DateCalculator } from '../utils/DateCalculator'; +import { DateService } from '../utils/DateService'; import { CoreEvents } from '../constants/CoreEvents'; import { NavigationRenderer } from '../renderers/NavigationRenderer'; import { GridRenderer } from '../renderers/GridRenderer'; @@ -14,18 +14,17 @@ export class NavigationManager { private eventBus: IEventBus; private navigationRenderer: NavigationRenderer; private gridRenderer: GridRenderer; - private dateCalculator: DateCalculator; + private dateService: DateService; private currentWeek: Date; private targetWeek: Date; private animationQueue: number = 0; constructor(eventBus: IEventBus, eventRenderer: EventRenderingService) { this.eventBus = eventBus; - DateCalculator.initialize(calendarConfig); - this.dateCalculator = new DateCalculator(); + this.dateService = new DateService('Europe/Copenhagen'); this.navigationRenderer = new NavigationRenderer(eventBus, eventRenderer); this.gridRenderer = new GridRenderer(); - this.currentWeek = DateCalculator.getISOWeekStart(new Date()); + this.currentWeek = this.getISOWeekStart(new Date()); this.targetWeek = new Date(this.currentWeek); this.init(); } @@ -34,6 +33,16 @@ export class NavigationManager { this.setupEventListeners(); } + /** + * Get the start of the ISO week (Monday) for a given date + * @param date - Any date in the week + * @returns The Monday of the ISO week + */ + private getISOWeekStart(date: Date): Date { + const weekBounds = this.dateService.getWeekBounds(date); + return this.dateService.startOfDay(weekBounds.start); + } + private getCalendarContainer(): HTMLElement | null { return document.querySelector('swp-calendar-container'); @@ -113,7 +122,7 @@ export class NavigationManager { * Navigate to specific event date and emit scroll event after navigation */ private navigateToEventDate(eventDate: Date, eventStartTime: string): void { - const weekStart = DateCalculator.getISOWeekStart(eventDate); + const weekStart = this.getISOWeekStart(eventDate); this.targetWeek = new Date(weekStart); const currentTime = this.currentWeek.getTime(); @@ -159,7 +168,7 @@ export class NavigationManager { private navigateToToday(): void { const today = new Date(); - const todayWeekStart = DateCalculator.getISOWeekStart(today); + const todayWeekStart = this.getISOWeekStart(today); // Reset to today this.targetWeek = new Date(todayWeekStart); @@ -177,7 +186,7 @@ export class NavigationManager { } private navigateToDate(date: Date): void { - const weekStart = DateCalculator.getISOWeekStart(date); + const weekStart = this.getISOWeekStart(date); this.targetWeek = new Date(weekStart); const currentTime = this.currentWeek.getTime(); @@ -277,9 +286,9 @@ export class NavigationManager { } private updateWeekInfo(): void { - const weekNumber = DateCalculator.getWeekNumber(this.currentWeek); - const weekEnd = DateCalculator.addDays(this.currentWeek, 6); - const dateRange = DateCalculator.formatDateRange(this.currentWeek, weekEnd); + const weekNumber = this.dateService.getWeekNumber(this.currentWeek); + const weekEnd = this.dateService.addDays(this.currentWeek, 6); + const dateRange = this.dateService.formatDateRange(this.currentWeek, weekEnd); // Notify other managers about week info update - DOM manipulation should happen via events this.eventBus.emit(CoreEvents.PERIOD_INFO_UPDATE, { diff --git a/src/managers/WorkHoursManager.ts b/src/managers/WorkHoursManager.ts index 23c5063..e12ac81 100644 --- a/src/managers/WorkHoursManager.ts +++ b/src/managers/WorkHoursManager.ts @@ -1,6 +1,6 @@ // Work hours management for per-column scheduling -import { DateCalculator } from '../utils/DateCalculator'; +import { DateService } from '../utils/DateService'; import { calendarConfig } from '../core/CalendarConfig'; import { PositionUtils } from '../utils/PositionUtils'; @@ -34,12 +34,12 @@ export interface WorkScheduleConfig { * Manages work hours scheduling with weekly defaults and date-specific overrides */ export class WorkHoursManager { - private dateCalculator: DateCalculator; + private dateService: DateService; private workSchedule: WorkScheduleConfig; constructor() { - DateCalculator.initialize(calendarConfig); - this.dateCalculator = new DateCalculator(); + const timezone = calendarConfig.getTimezone?.() || 'Europe/Copenhagen'; + this.dateService = new DateService(timezone); // Default work schedule - will be loaded from JSON later this.workSchedule = { @@ -64,7 +64,7 @@ export class WorkHoursManager { * Get work hours for a specific date */ getWorkHoursForDate(date: Date): DayWorkHours | 'off' { - const dateString = DateCalculator.formatISODate(date); + const dateString = this.dateService.formatISODate(date); // Check for date-specific override first if (this.workSchedule.dateOverrides[dateString]) { @@ -83,7 +83,7 @@ export class WorkHoursManager { const workHoursMap = new Map(); dates.forEach(date => { - const dateString = DateCalculator.formatISODate(date); + const dateString = this.dateService.formatISODate(date); const workHours = this.getWorkHoursForDate(date); workHoursMap.set(dateString, workHours); }); diff --git a/src/renderers/ColumnRenderer.ts b/src/renderers/ColumnRenderer.ts index 58a75de..12b7dc6 100644 --- a/src/renderers/ColumnRenderer.ts +++ b/src/renderers/ColumnRenderer.ts @@ -2,7 +2,7 @@ import { CalendarConfig } from '../core/CalendarConfig'; import { ResourceCalendarData } from '../types/CalendarTypes'; -import { DateCalculator } from '../utils/DateCalculator'; +import { DateService } from '../utils/DateService'; import { WorkHoursManager } from '../managers/WorkHoursManager'; /** @@ -25,25 +25,26 @@ export interface ColumnRenderContext { * Date-based column renderer (original functionality) */ export class DateColumnRenderer implements ColumnRenderer { - private dateCalculator!: DateCalculator; + private dateService!: DateService; private workHoursManager!: WorkHoursManager; render(columnContainer: HTMLElement, context: ColumnRenderContext): void { const { currentWeek, config } = context; - // Initialize date calculator and work hours manager - DateCalculator.initialize(config); - this.dateCalculator = new DateCalculator(); + // Initialize date service and work hours manager + const timezone = config.getTimezone?.() || 'Europe/Copenhagen'; + this.dateService = new DateService(timezone); this.workHoursManager = new WorkHoursManager(); - const dates = DateCalculator.getWorkWeekDates(currentWeek); + const workWeekSettings = config.getWorkWeekSettings(); + const dates = this.dateService.getWorkWeekDates(currentWeek, workWeekSettings.workDays); const dateSettings = config.getDateViewSettings(); const daysToShow = dates.slice(0, dateSettings.weekDays); daysToShow.forEach((date) => { const column = document.createElement('swp-day-column'); - (column as any).dataset.date = DateCalculator.formatISODate(date); + (column as any).dataset.date = this.dateService.formatISODate(date); // Apply work hours styling this.applyWorkHoursToColumn(column, date); diff --git a/src/renderers/EventRenderer.ts b/src/renderers/EventRenderer.ts index 7ffa8d8..efbe961 100644 --- a/src/renderers/EventRenderer.ts +++ b/src/renderers/EventRenderer.ts @@ -2,7 +2,6 @@ import { CalendarEvent } from '../types/CalendarTypes'; import { calendarConfig } from '../core/CalendarConfig'; -import { DateCalculator } from '../utils/DateCalculator'; import { eventBus } from '../core/EventBus'; import { OverlapDetector, OverlapResult } from '../utils/OverlapDetector'; import { SwpEventElement } from '../elements/SwpEventElement'; @@ -38,18 +37,13 @@ export interface EventRendererStrategy { */ export class DateEventRenderer implements EventRendererStrategy { + private dateService: DateService; - constructor(dateCalculator?: DateCalculator) { - - if (!dateCalculator) { - DateCalculator.initialize(calendarConfig); - } - this.dateCalculator = dateCalculator || new DateCalculator(); - - + constructor() { + const timezone = calendarConfig.getTimezone?.() || 'Europe/Copenhagen'; + this.dateService = new DateService(timezone); this.setupDragEventListeners(); } - private dateCalculator: DateCalculator; private draggedClone: HTMLElement | null = null; private originalEvent: HTMLElement | null = null; @@ -634,10 +628,9 @@ export class DateEventRenderer implements EventRendererStrategy { } const columnEvents = events.filter(event => { - const eventDateStr = DateCalculator.formatISODate(event.start); + const eventDateStr = this.dateService.formatISODate(event.start); const matches = eventDateStr === columnDate; - return matches; }); diff --git a/src/renderers/GridRenderer.ts b/src/renderers/GridRenderer.ts index 2e4d32a..93ec0c8 100644 --- a/src/renderers/GridRenderer.ts +++ b/src/renderers/GridRenderer.ts @@ -3,7 +3,7 @@ import { ResourceCalendarData, CalendarView } from '../types/CalendarTypes'; import { CalendarTypeFactory } from '../factories/CalendarTypeFactory'; import { ColumnRenderContext } from './ColumnRenderer'; import { eventBus } from '../core/EventBus'; -import { DateCalculator } from '../utils/DateCalculator'; +import { DateService } from '../utils/DateService'; import { CoreEvents } from '../constants/CoreEvents'; /** @@ -13,8 +13,11 @@ import { CoreEvents } from '../constants/CoreEvents'; export class GridRenderer { private cachedGridContainer: HTMLElement | null = null; private cachedTimeAxis: HTMLElement | null = null; + private dateService: DateService; constructor() { + const timezone = calendarConfig.getTimezone?.() || 'Europe/Copenhagen'; + this.dateService = new DateService(timezone); } /** @@ -239,7 +242,7 @@ export class GridRenderer { console.log('Parent container:', parentContainer); console.log('Using same grid creation as initial load'); - const weekEnd = DateCalculator.addDays(weekStart, 6); + const weekEnd = this.dateService.addDays(weekStart, 6); // Use SAME method as initial load - respects workweek and resource settings const newGrid = this.createOptimizedGridContainer(weekStart, null, 'week'); diff --git a/src/renderers/HeaderRenderer.ts b/src/renderers/HeaderRenderer.ts index c06daeb..77ba231 100644 --- a/src/renderers/HeaderRenderer.ts +++ b/src/renderers/HeaderRenderer.ts @@ -2,7 +2,7 @@ import { CalendarConfig } from '../core/CalendarConfig'; import { ResourceCalendarData } from '../types/CalendarTypes'; -import { DateCalculator } from '../utils/DateCalculator'; +import { DateService } from '../utils/DateService'; /** * Interface for header rendering strategies @@ -25,7 +25,7 @@ export interface HeaderRenderContext { * Date-based header renderer (original functionality) */ export class DateHeaderRenderer implements HeaderRenderer { - private dateCalculator!: DateCalculator; + private dateService!: DateService; render(calendarHeader: HTMLElement, context: HeaderRenderContext): void { const { currentWeek, config } = context; @@ -34,27 +34,28 @@ export class DateHeaderRenderer implements HeaderRenderer { const allDayContainer = document.createElement('swp-allday-container'); calendarHeader.appendChild(allDayContainer); - // Initialize date calculator with config - DateCalculator.initialize(config); - this.dateCalculator = new DateCalculator(); + // Initialize date service with config + const timezone = config.getTimezone?.() || 'Europe/Copenhagen'; + this.dateService = new DateService(timezone); - const dates = DateCalculator.getWorkWeekDates(currentWeek); + const workWeekSettings = config.getWorkWeekSettings(); + const dates = this.dateService.getWorkWeekDates(currentWeek, workWeekSettings.workDays); const weekDays = config.getDateViewSettings().weekDays; const daysToShow = dates.slice(0, weekDays); daysToShow.forEach((date, index) => { const header = document.createElement('swp-day-header'); - if (DateCalculator.isToday(date)) { + if (this.dateService.isSameDay(date, new Date())) { (header as any).dataset.today = 'true'; } - const dayName = DateCalculator.getDayName(date, 'short'); + const dayName = this.dateService.getDayName(date, 'short'); header.innerHTML = ` ${dayName} ${date.getDate()} `; - (header as any).dataset.date = DateCalculator.formatISODate(date); + (header as any).dataset.date = this.dateService.formatISODate(date); calendarHeader.appendChild(header); }); diff --git a/src/strategies/MonthViewStrategy.ts b/src/strategies/MonthViewStrategy.ts index 7585ecb..1c3f18b 100644 --- a/src/strategies/MonthViewStrategy.ts +++ b/src/strategies/MonthViewStrategy.ts @@ -4,16 +4,15 @@ */ import { ViewStrategy, ViewContext, ViewLayoutConfig } from './ViewStrategy'; -import { DateCalculator } from '../utils/DateCalculator'; +import { DateService } from '../utils/DateService'; import { calendarConfig } from '../core/CalendarConfig'; import { CalendarEvent } from '../types/CalendarTypes'; export class MonthViewStrategy implements ViewStrategy { - private dateCalculator: DateCalculator; + private dateService: DateService; constructor() { - DateCalculator.initialize(calendarConfig); - this.dateCalculator = new DateCalculator(); + this.dateService = new DateService('Europe/Copenhagen'); } getLayoutConfig(): ViewLayoutConfig { @@ -74,7 +73,7 @@ export class MonthViewStrategy implements ViewStrategy { dates.forEach(date => { const cell = document.createElement('div'); cell.className = 'month-day-cell'; - cell.dataset.date = DateCalculator.formatISODate(date); + cell.dataset.date = this.dateService.formatISODate(date); cell.style.border = '1px solid #e0e0e0'; cell.style.minHeight = '100px'; cell.style.padding = '4px'; @@ -88,7 +87,7 @@ export class MonthViewStrategy implements ViewStrategy { dayNumber.style.marginBottom = '4px'; // Check if today - if (DateCalculator.isToday(date)) { + if (this.dateService.isSameDay(date, new Date())) { dayNumber.style.color = '#1976d2'; cell.style.backgroundColor = '#f5f5f5'; } @@ -103,12 +102,13 @@ export class MonthViewStrategy implements ViewStrategy { const firstOfMonth = new Date(monthDate.getFullYear(), monthDate.getMonth(), 1); // Get Monday of the week containing first day - const startDate = DateCalculator.getISOWeekStart(firstOfMonth); + const weekBounds = this.dateService.getWeekBounds(firstOfMonth); + const startDate = this.dateService.startOfDay(weekBounds.start); // Generate 42 days (6 weeks) const dates: Date[] = []; for (let i = 0; i < 42; i++) { - dates.push(DateCalculator.addDays(startDate, i)); + dates.push(this.dateService.addDays(startDate, i)); } return dates; @@ -143,10 +143,11 @@ export class MonthViewStrategy implements ViewStrategy { const firstOfMonth = new Date(baseDate.getFullYear(), baseDate.getMonth(), 1); // Get Monday of the week containing first day - const startDate = DateCalculator.getISOWeekStart(firstOfMonth); + const weekBounds = this.dateService.getWeekBounds(firstOfMonth); + const startDate = this.dateService.startOfDay(weekBounds.start); // End date is 41 days after start (42 total days) - const endDate = DateCalculator.addDays(startDate, 41); + const endDate = this.dateService.addDays(startDate, 41); return { startDate, diff --git a/src/strategies/WeekViewStrategy.ts b/src/strategies/WeekViewStrategy.ts index 5366afd..db19c5c 100644 --- a/src/strategies/WeekViewStrategy.ts +++ b/src/strategies/WeekViewStrategy.ts @@ -4,19 +4,19 @@ */ import { ViewStrategy, ViewContext, ViewLayoutConfig } from './ViewStrategy'; -import { DateCalculator } from '../utils/DateCalculator'; +import { DateService } from '../utils/DateService'; import { calendarConfig } from '../core/CalendarConfig'; import { GridRenderer } from '../renderers/GridRenderer'; import { GridStyleManager } from '../renderers/GridStyleManager'; export class WeekViewStrategy implements ViewStrategy { - private dateCalculator: DateCalculator; + private dateService: DateService; private gridRenderer: GridRenderer; private styleManager: GridStyleManager; constructor() { - DateCalculator.initialize(calendarConfig); - this.dateCalculator = new DateCalculator(); + const timezone = calendarConfig.getTimezone?.() || 'Europe/Copenhagen'; + this.dateService = new DateService(timezone); this.gridRenderer = new GridRenderer(); this.styleManager = new GridStyleManager(); } @@ -43,28 +43,31 @@ export class WeekViewStrategy implements ViewStrategy { } getNextPeriod(currentDate: Date): Date { - return DateCalculator.addWeeks(currentDate, 1); + return this.dateService.addWeeks(currentDate, 1); } getPreviousPeriod(currentDate: Date): Date { - return DateCalculator.addWeeks(currentDate, -1); + return this.dateService.addWeeks(currentDate, -1); } getPeriodLabel(date: Date): string { - const weekStart = DateCalculator.getISOWeekStart(date); - const weekEnd = DateCalculator.addDays(weekStart, 6); - const weekNumber = DateCalculator.getWeekNumber(date); + const weekBounds = this.dateService.getWeekBounds(date); + const weekStart = this.dateService.startOfDay(weekBounds.start); + const weekEnd = this.dateService.addDays(weekStart, 6); + const weekNumber = this.dateService.getWeekNumber(date); - return `Week ${weekNumber}: ${DateCalculator.formatDateRange(weekStart, weekEnd)}`; + return `Week ${weekNumber}: ${this.dateService.formatDateRange(weekStart, weekEnd)}`; } getDisplayDates(baseDate: Date): Date[] { - return DateCalculator.getWorkWeekDates(baseDate); + const workWeekSettings = calendarConfig.getWorkWeekSettings(); + return this.dateService.getWorkWeekDates(baseDate, workWeekSettings.workDays); } getPeriodRange(baseDate: Date): { startDate: Date; endDate: Date } { - const weekStart = DateCalculator.getISOWeekStart(baseDate); - const weekEnd = DateCalculator.addDays(weekStart, 6); + const weekBounds = this.dateService.getWeekBounds(baseDate); + const weekStart = this.dateService.startOfDay(weekBounds.start); + const weekEnd = this.dateService.addDays(weekStart, 6); return { startDate: weekStart, diff --git a/src/utils/DateCalculator.ts b/src/utils/DateCalculator.ts deleted file mode 100644 index d9d5d37..0000000 --- a/src/utils/DateCalculator.ts +++ /dev/null @@ -1,300 +0,0 @@ -/** - * DateCalculator - Centralized date calculation logic for calendar - * Now uses DateService internally for all date operations - * Handles all date computations with proper week start handling - */ - -import { CalendarConfig } from '../core/CalendarConfig'; -import { DateService } from './DateService'; - -export class DateCalculator { - private static config: CalendarConfig; - private static dateService: DateService = new DateService('Europe/Copenhagen'); - - /** - * Initialize DateCalculator with configuration - * @param config - Calendar configuration - */ - static initialize(config: CalendarConfig): void { - DateCalculator.config = config; - // Update DateService with timezone from config if available - const timezone = config.getTimezone?.() || 'Europe/Copenhagen'; - DateCalculator.dateService = new DateService(timezone); - } - - /** - * Validate that a date is valid - * @param date - Date to validate - * @returns True if date is valid, false otherwise - */ - private static validateDate(date: Date): boolean { - return date && date instanceof Date && DateCalculator.dateService.isValid(date); - } - - /** - * 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 - */ - static getWorkWeekDates(weekStart: Date): Date[] { - if (!DateCalculator.validateDate(weekStart)) { - throw new Error('getWorkWeekDates: Invalid date provided'); - } - - const dates: Date[] = []; - const workWeekSettings = DateCalculator.config.getWorkWeekSettings(); - - // Always use ISO week start (Monday) - const mondayOfWeek = DateCalculator.getISOWeekStart(weekStart); - - // 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); - }); - - return dates; - } - - /** - * Get the start of the ISO week (Monday) for a given date using DateService - * @param date - Any date in the week - * @returns The Monday of the ISO week - */ - static getISOWeekStart(date: Date): Date { - if (!DateCalculator.validateDate(date)) { - throw new Error('getISOWeekStart: Invalid date provided'); - } - - const weekBounds = DateCalculator.dateService.getWeekBounds(date); - return DateCalculator.dateService.startOfDay(weekBounds.start); - } - - /** - * Get the end of the ISO week for a given date using DateService - * @param date - Any date in the week - * @returns The end date of the ISO week (Sunday) - */ - static getWeekEnd(date: Date): Date { - if (!DateCalculator.validateDate(date)) { - throw new Error('getWeekEnd: Invalid date provided'); - } - - const weekBounds = DateCalculator.dateService.getWeekBounds(date); - return DateCalculator.dateService.endOfDay(weekBounds.end); - } - - /** - * Get week number for a date (ISO 8601) - * @param date - The date to get week number for - * @returns Week number (1-53) - */ - 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); - } - - /** - * Format a date range with customizable options - * @param start - Start date - * @param end - End date - * @param options - Formatting options - * @returns Formatted date range string - */ - static 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 startYear = start.getFullYear(); - const endYear = end.getFullYear(); - - 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)}`; - } - - /** - * Format a date to ISO date string (YYYY-MM-DD) using DateService - * @param date - Date to format - * @returns ISO date string or empty string if invalid - */ - static formatISODate(date: Date): string { - if (!DateCalculator.validateDate(date)) { - return ''; - } - return DateCalculator.dateService.formatDate(date); - } - - /** - * Check if a date is today using DateService - * @param date - Date to check - * @returns True if the date is today - */ - static isToday(date: Date): boolean { - return DateCalculator.dateService.isSameDay(date, new Date()); - } - - /** - * Add days to a date using DateService - * @param date - Base date - * @param days - Number of days to add (can be negative) - * @returns New date - */ - static addDays(date: Date, days: number): Date { - return DateCalculator.dateService.addDays(date, days); - } - - /** - * Add weeks to a date using DateService - * @param date - Base date - * @param weeks - Number of weeks to add (can be negative) - * @returns New date - */ - static addWeeks(date: Date, weeks: number): Date { - return DateCalculator.dateService.addWeeks(date, weeks); - } - - /** - * Get all dates in a week - * @param weekStart - Start of the week - * @returns Array of 7 dates for the full week - */ - static getFullWeekDates(weekStart: Date): Date[] { - const dates: Date[] = []; - for (let i = 0; i < 7; i++) { - dates.push(DateCalculator.addDays(weekStart, i)); - } - return dates; - } - - /** - * 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 - */ - static getDayName(date: Date, format: 'short' | 'long' = 'short'): string { - const formatter = new Intl.DateTimeFormat('en-US', { - weekday: format - }); - return formatter.format(date); - } - - /** - * Format time to HH:MM using DateService - * @param date - Date to format - * @returns Time string - */ - static formatTime(date: Date): string { - return DateCalculator.dateService.formatTime(date); - } - - /** - * Format time to 12-hour format - * @param date - Date to format - * @returns 12-hour time string - */ - 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 using DateService - * @param minutes - Minutes since midnight - * @returns Time string - */ - static minutesToTime(minutes: number): string { - return DateCalculator.dateService.minutesToTime(minutes); - } - - /** - * Convert time string to minutes since midnight using DateService - * @param timeStr - Time string - * @returns Minutes since midnight - */ - static timeToMinutes(timeStr: string): number { - return DateCalculator.dateService.timeToMinutes(timeStr); - } - - /** - * Get minutes since start of day using DateService - * @param date - Date or ISO string - * @returns Minutes since midnight - */ - static getMinutesSinceMidnight(date: Date | string): number { - const d = typeof date === 'string' ? DateCalculator.dateService.parseISO(date) : date; - return DateCalculator.dateService.getMinutesSinceMidnight(d); - } - - /** - * Calculate duration in minutes between two dates using DateService - * @param start - Start date or ISO string - * @param end - End date or ISO string - * @returns Duration in minutes - */ - static getDurationMinutes(start: Date | string, end: Date | string): number { - return DateCalculator.dateService.getDurationMinutes(start, end); - } - - /** - * Check if two dates are on the same day using DateService - * @param date1 - First date - * @param date2 - Second date - * @returns True if same day - */ - static isSameDay(date1: Date, date2: Date): boolean { - return DateCalculator.dateService.isSameDay(date1, date2); - } - - /** - * 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 - */ - static isMultiDay(start: Date | string, end: Date | string): boolean { - const startDate = typeof start === 'string' ? DateCalculator.dateService.parseISO(start) : start; - const endDate = typeof end === 'string' ? DateCalculator.dateService.parseISO(end) : end; - return !DateCalculator.isSameDay(startDate, endDate); - } - - // Legacy constructor for backward compatibility - constructor() { - // Empty constructor - all methods are now static - } -} - -// Legacy factory function - deprecated, use static methods instead -export function createDateCalculator(config: CalendarConfig): DateCalculator { - DateCalculator.initialize(config); - return new DateCalculator(); -} \ No newline at end of file diff --git a/src/utils/PositionUtils.ts b/src/utils/PositionUtils.ts index 5f6626d..218987d 100644 --- a/src/utils/PositionUtils.ts +++ b/src/utils/PositionUtils.ts @@ -1,15 +1,17 @@ import { calendarConfig } from '../core/CalendarConfig'; import { ColumnBounds } from './ColumnDetectionUtils'; -import { DateCalculator } from './DateCalculator'; +import { DateService } from './DateService'; import { TimeFormatter } from './TimeFormatter'; /** * PositionUtils - Static positioning utilities using singleton calendarConfig * Focuses on pixel/position calculations while delegating date operations * - * Note: Uses DateCalculator and TimeFormatter which internally use DateService with date-fns + * Note: Uses DateService with date-fns for all date/time operations */ export class PositionUtils { + private static dateService = new DateService('Europe/Copenhagen'); + /** * Convert minutes to pixels */ @@ -29,10 +31,10 @@ export class PositionUtils { } /** - * Convert time (HH:MM) to pixels from day start using DateCalculator + * Convert time (HH:MM) to pixels from day start using DateService */ public static timeToPixels(timeString: string): number { - const totalMinutes = DateCalculator.timeToMinutes(timeString); + const totalMinutes = PositionUtils.dateService.timeToMinutes(timeString); const gridSettings = calendarConfig.getGridSettings(); const dayStartMinutes = gridSettings.dayStartHour * 60; const minutesFromDayStart = totalMinutes - dayStartMinutes; @@ -41,10 +43,10 @@ export class PositionUtils { } /** - * Convert Date object to pixels from day start using DateCalculator + * Convert Date object to pixels from day start using DateService */ public static dateToPixels(date: Date): number { - const totalMinutes = DateCalculator.getMinutesSinceMidnight(date); + const totalMinutes = PositionUtils.dateService.getMinutesSinceMidnight(date); const gridSettings = calendarConfig.getGridSettings(); const dayStartMinutes = gridSettings.dayStartHour * 60; const minutesFromDayStart = totalMinutes - dayStartMinutes; @@ -53,7 +55,7 @@ export class PositionUtils { } /** - * Convert pixels to time using DateCalculator + * Convert pixels to time using DateService */ public static pixelsToTime(pixels: number): string { const minutes = PositionUtils.pixelsToMinutes(pixels); @@ -61,7 +63,7 @@ export class PositionUtils { const dayStartMinutes = gridSettings.dayStartHour * 60; const totalMinutes = dayStartMinutes + minutes; - return DateCalculator.minutesToTime(totalMinutes); + return PositionUtils.dateService.minutesToTime(totalMinutes); } /** @@ -109,15 +111,15 @@ export class PositionUtils { } /** - * Snap time to interval using DateCalculator + * Snap time to interval using DateService */ public static snapTimeToInterval(timeString: string): string { - const totalMinutes = DateCalculator.timeToMinutes(timeString); + const totalMinutes = PositionUtils.dateService.timeToMinutes(timeString); const gridSettings = calendarConfig.getGridSettings(); const snapInterval = gridSettings.snapInterval; const snappedMinutes = Math.round(totalMinutes / snapInterval) * snapInterval; - return DateCalculator.minutesToTime(snappedMinutes); + return PositionUtils.dateService.minutesToTime(snappedMinutes); } /** @@ -220,10 +222,10 @@ export class PositionUtils { } /** - * Convert time string to ISO datetime using DateCalculator + * Convert time string to ISO datetime using DateService */ public static timeStringToIso(timeString: string, date: Date = new Date()): string { - const totalMinutes = DateCalculator.timeToMinutes(timeString); + const totalMinutes = PositionUtils.dateService.timeToMinutes(timeString); const hours = Math.floor(totalMinutes / 60); const minutes = totalMinutes % 60; @@ -234,10 +236,10 @@ export class PositionUtils { } /** - * Calculate event duration using DateCalculator + * Calculate event duration using DateService */ public static calculateDuration(startTime: string | Date, endTime: string | Date): number { - return DateCalculator.getDurationMinutes(startTime, endTime); + return PositionUtils.dateService.getDurationMinutes(startTime, endTime); } /** diff --git a/test/utils/DateCalculator.test.ts b/test/utils/DateCalculator.test.ts deleted file mode 100644 index 72f6035..0000000 --- a/test/utils/DateCalculator.test.ts +++ /dev/null @@ -1,310 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest'; -import { DateCalculator } from '../../src/utils/DateCalculator'; -import { CalendarConfig } from '../../src/core/CalendarConfig'; - -describe('DateCalculator', () => { - let testConfig: CalendarConfig; - - beforeEach(() => { - testConfig = new CalendarConfig(); - DateCalculator.initialize(testConfig); - }); - - describe('Week Operations', () => { - it('should get ISO week start (Monday)', () => { - // Wednesday, January 17, 2024 - const date = new Date(2024, 0, 17); - const weekStart = DateCalculator.getISOWeekStart(date); - - // Should be Monday, January 15 - expect(weekStart.getDate()).toBe(15); - expect(weekStart.getDay()).toBe(1); // Monday - expect(weekStart.getHours()).toBe(0); - expect(weekStart.getMinutes()).toBe(0); - }); - - it('should get ISO week start for Sunday', () => { - // Sunday, January 21, 2024 - const date = new Date(2024, 0, 21); - const weekStart = DateCalculator.getISOWeekStart(date); - - // Should be Monday, January 15 - expect(weekStart.getDate()).toBe(15); - expect(weekStart.getDay()).toBe(1); - }); - - it('should get week end (Sunday)', () => { - // Wednesday, January 17, 2024 - const date = new Date(2024, 0, 17); - const weekEnd = DateCalculator.getWeekEnd(date); - - // Should be Sunday, January 21 - expect(weekEnd.getDate()).toBe(21); - expect(weekEnd.getDay()).toBe(0); // Sunday - expect(weekEnd.getHours()).toBe(23); - expect(weekEnd.getMinutes()).toBe(59); - }); - - it('should get work week dates (Mon-Fri)', () => { - const date = new Date(2024, 0, 17); // Wednesday - const workDays = DateCalculator.getWorkWeekDates(date); - - expect(workDays).toHaveLength(5); - expect(workDays[0].getDay()).toBe(1); // Monday - expect(workDays[4].getDay()).toBe(5); // Friday - }); - - it('should get full week dates (7 days)', () => { - const weekStart = new Date(2024, 0, 15); // Monday - const fullWeek = DateCalculator.getFullWeekDates(weekStart); - - expect(fullWeek).toHaveLength(7); - expect(fullWeek[0].getDay()).toBe(1); // Monday - expect(fullWeek[6].getDay()).toBe(0); // Sunday - }); - - it('should calculate ISO week number', () => { - const date1 = new Date(2024, 0, 1); // January 1, 2024 - const weekNum1 = DateCalculator.getWeekNumber(date1); - expect(weekNum1).toBe(1); - - const date2 = new Date(2024, 0, 15); // January 15, 2024 - const weekNum2 = DateCalculator.getWeekNumber(date2); - expect(weekNum2).toBe(3); - }); - - it('should handle year boundary for week numbers', () => { - const date = new Date(2023, 11, 31); // December 31, 2023 - const weekNum = DateCalculator.getWeekNumber(date); - // Week 52 or 53 depending on year - expect(weekNum).toBeGreaterThanOrEqual(52); - }); - }); - - describe('Date Manipulation', () => { - it('should add days', () => { - const date = new Date(2024, 0, 15); - const newDate = DateCalculator.addDays(date, 5); - - expect(newDate.getDate()).toBe(20); - expect(newDate.getMonth()).toBe(0); - }); - - it('should subtract days', () => { - const date = new Date(2024, 0, 15); - const newDate = DateCalculator.addDays(date, -5); - - expect(newDate.getDate()).toBe(10); - }); - - it('should add weeks', () => { - const date = new Date(2024, 0, 15); - const newDate = DateCalculator.addWeeks(date, 2); - - expect(newDate.getDate()).toBe(29); - }); - - it('should subtract weeks', () => { - const date = new Date(2024, 0, 15); - const newDate = DateCalculator.addWeeks(date, -1); - - expect(newDate.getDate()).toBe(8); - }); - - it('should handle month boundaries when adding days', () => { - const date = new Date(2024, 0, 30); // January 30 - const newDate = DateCalculator.addDays(date, 5); - - expect(newDate.getDate()).toBe(4); // February 4 - expect(newDate.getMonth()).toBe(1); - }); - }); - - describe('Time Formatting', () => { - it('should format time (24-hour)', () => { - const date = new Date(2024, 0, 15, 14, 30, 45); - const formatted = DateCalculator.formatTime(date); - - expect(formatted).toBe('14:30'); - }); - - it('should format time (12-hour)', () => { - const date1 = new Date(2024, 0, 15, 14, 30, 0); - const formatted1 = DateCalculator.formatTime12(date1); - expect(formatted1).toBe('2:30 PM'); - - const date2 = new Date(2024, 0, 15, 9, 15, 0); - const formatted2 = DateCalculator.formatTime12(date2); - expect(formatted2).toBe('9:15 AM'); - - const date3 = new Date(2024, 0, 15, 0, 0, 0); - const formatted3 = DateCalculator.formatTime12(date3); - expect(formatted3).toBe('12:00 AM'); - }); - - it('should format ISO date', () => { - const date = new Date(2024, 0, 15, 14, 30, 0); - const formatted = DateCalculator.formatISODate(date); - - expect(formatted).toBe('2024-01-15'); - }); - - it('should format date range', () => { - const start = new Date(2024, 0, 15); - const end = new Date(2024, 0, 21); - const formatted = DateCalculator.formatDateRange(start, end); - - expect(formatted).toContain('Jan'); - expect(formatted).toContain('15'); - expect(formatted).toContain('21'); - }); - - it('should get day name (short)', () => { - const monday = new Date(2024, 0, 15); // Monday - const dayName = DateCalculator.getDayName(monday, 'short'); - - expect(dayName).toBe('Mon'); - }); - - it('should get day name (long)', () => { - const monday = new Date(2024, 0, 15); // Monday - const dayName = DateCalculator.getDayName(monday, 'long'); - - expect(dayName).toBe('Monday'); - }); - }); - - describe('Time Calculations', () => { - it('should convert time string to minutes', () => { - expect(DateCalculator.timeToMinutes('09:00')).toBe(540); - expect(DateCalculator.timeToMinutes('14:30')).toBe(870); - expect(DateCalculator.timeToMinutes('00:00')).toBe(0); - expect(DateCalculator.timeToMinutes('23:59')).toBe(1439); - }); - - it('should convert minutes to time string', () => { - expect(DateCalculator.minutesToTime(540)).toBe('09:00'); - expect(DateCalculator.minutesToTime(870)).toBe('14:30'); - expect(DateCalculator.minutesToTime(0)).toBe('00:00'); - expect(DateCalculator.minutesToTime(1439)).toBe('23:59'); - }); - - it('should get minutes since midnight from Date', () => { - const date = new Date(2024, 0, 15, 14, 30, 0); - const minutes = DateCalculator.getMinutesSinceMidnight(date); - - expect(minutes).toBe(870); // 14*60 + 30 - }); - - it('should get minutes since midnight from ISO string', () => { - const isoString = '2024-01-15T14:30:00.000Z'; - const minutes = DateCalculator.getMinutesSinceMidnight(isoString); - - // Note: This will be in local time after parsing - expect(minutes).toBeGreaterThanOrEqual(0); - expect(minutes).toBeLessThan(1440); - }); - - it('should calculate duration in minutes', () => { - const start = new Date(2024, 0, 15, 9, 0, 0); - const end = new Date(2024, 0, 15, 10, 30, 0); - const duration = DateCalculator.getDurationMinutes(start, end); - - expect(duration).toBe(90); - }); - - it('should calculate duration from ISO strings', () => { - const start = '2024-01-15T09:00:00.000Z'; - const end = '2024-01-15T10:30:00.000Z'; - const duration = DateCalculator.getDurationMinutes(start, end); - - expect(duration).toBe(90); - }); - - it('should handle cross-midnight duration', () => { - const start = new Date(2024, 0, 15, 23, 0, 0); - const end = new Date(2024, 0, 16, 1, 0, 0); - const duration = DateCalculator.getDurationMinutes(start, end); - - expect(duration).toBe(120); // 2 hours - }); - }); - - describe('Date Comparisons', () => { - it('should check if date is today', () => { - const today = new Date(); - const yesterday = DateCalculator.addDays(new Date(), -1); - - expect(DateCalculator.isToday(today)).toBe(true); - expect(DateCalculator.isToday(yesterday)).toBe(false); - }); - - it('should check if same day', () => { - const date1 = new Date(2024, 0, 15, 10, 0, 0); - const date2 = new Date(2024, 0, 15, 14, 30, 0); - const date3 = new Date(2024, 0, 16, 10, 0, 0); - - expect(DateCalculator.isSameDay(date1, date2)).toBe(true); - expect(DateCalculator.isSameDay(date1, date3)).toBe(false); - }); - - it('should check if multi-day event (Date objects)', () => { - const start = new Date(2024, 0, 15, 10, 0, 0); - const end1 = new Date(2024, 0, 15, 14, 0, 0); - const end2 = new Date(2024, 0, 16, 10, 0, 0); - - expect(DateCalculator.isMultiDay(start, end1)).toBe(false); - expect(DateCalculator.isMultiDay(start, end2)).toBe(true); - }); - - it('should check if multi-day event (ISO strings)', () => { - const start = '2024-01-15T10:00:00.000Z'; - const end1 = '2024-01-15T14:00:00.000Z'; - const end2 = '2024-01-16T10:00:00.000Z'; - - expect(DateCalculator.isMultiDay(start, end1)).toBe(false); - expect(DateCalculator.isMultiDay(start, end2)).toBe(true); - }); - }); - - describe('Edge Cases', () => { - it('should handle midnight', () => { - const date = new Date(2024, 0, 15, 0, 0, 0); - const minutes = DateCalculator.getMinutesSinceMidnight(date); - - expect(minutes).toBe(0); - }); - - it('should handle end of day', () => { - const date = new Date(2024, 0, 15, 23, 59, 0); - const minutes = DateCalculator.getMinutesSinceMidnight(date); - - expect(minutes).toBe(1439); - }); - - it('should handle leap year', () => { - const date = new Date(2024, 1, 29); // February 29, 2024 (leap year) - const nextDay = DateCalculator.addDays(date, 1); - - expect(nextDay.getDate()).toBe(1); // March 1 - expect(nextDay.getMonth()).toBe(2); - }); - - it('should handle DST transitions', () => { - // This test depends on timezone, but we test the basic functionality - const beforeDST = new Date(2024, 2, 30); // March 30, 2024 - const afterDST = DateCalculator.addDays(beforeDST, 1); - - expect(afterDST.getDate()).toBe(31); - }); - }); - - describe('Error Handling', () => { - it('should handle invalid dates gracefully', () => { - const invalidDate = new Date('invalid'); - - const result = DateCalculator.formatISODate(invalidDate); - expect(result).toBe(''); - }); - }); -}); \ No newline at end of file