diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 809ecbf..d512ba3 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -8,7 +8,8 @@ "Bash(find:*)", "Bash(mv:*)", "Bash(rm:*)", - "Bash(npm install:*)" + "Bash(npm install:*)", + "Bash(npm test)" ], "deny": [] } diff --git a/src/elements/SwpEventElement.ts b/src/elements/SwpEventElement.ts index b8e7dc7..02ec85e 100644 --- a/src/elements/SwpEventElement.ts +++ b/src/elements/SwpEventElement.ts @@ -1,5 +1,5 @@ import { CalendarEvent } from '../types/CalendarTypes'; -import { calendarConfig } from '../core/CalendarConfig'; +import { CalendarConfig } from '../core/CalendarConfig'; import { TimeFormatter } from '../utils/TimeFormatter'; import { PositionUtils } from '../utils/PositionUtils'; import { DateService } from '../utils/DateService'; @@ -9,11 +9,13 @@ import { DateService } from '../utils/DateService'; */ export abstract class BaseSwpEventElement extends HTMLElement { protected dateService: DateService; + protected config: CalendarConfig; constructor() { super(); - const timezone = calendarConfig.getTimezone?.(); - this.dateService = new DateService(timezone); + // TODO: Find better solution for web component DI + this.config = new CalendarConfig(); + this.dateService = new DateService(this.config); } // ============================================ @@ -135,7 +137,7 @@ export class SwpEventElement extends BaseSwpEventElement { this.style.height = `${newHeight}px`; // 2. Calculate new end time based on height - const gridSettings = calendarConfig.getGridSettings(); + const gridSettings = this.config.getGridSettings(); const { hourHeight, snapInterval } = gridSettings; // Get current start time @@ -228,7 +230,7 @@ export class SwpEventElement extends BaseSwpEventElement { * Calculate start/end minutes from Y position */ private calculateTimesFromPosition(snappedY: number): { startMinutes: number; endMinutes: number } { - const gridSettings = calendarConfig.getGridSettings(); + const gridSettings = this.config.getGridSettings(); const { hourHeight, dayStartHour, snapInterval } = gridSettings; // Get original duration @@ -258,8 +260,8 @@ export class SwpEventElement extends BaseSwpEventElement { */ public static fromCalendarEvent(event: CalendarEvent): SwpEventElement { const element = document.createElement('swp-event') as SwpEventElement; - const timezone = calendarConfig.getTimezone?.(); - const dateService = new DateService(timezone); + const config = new CalendarConfig(); + const dateService = new DateService(config); element.dataset.eventId = event.id; element.dataset.title = event.title; @@ -333,8 +335,8 @@ export class SwpAllDayEventElement extends BaseSwpEventElement { */ public static fromCalendarEvent(event: CalendarEvent): SwpAllDayEventElement { const element = document.createElement('swp-allday-event') as SwpAllDayEventElement; - const timezone = calendarConfig.getTimezone?.(); - const dateService = new DateService(timezone); + const config = new CalendarConfig(); + const dateService = new DateService(config); element.dataset.eventId = event.id; element.dataset.title = event.title; diff --git a/src/index.ts b/src/index.ts index 5f3a9fe..a02be2d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,15 +24,25 @@ import { HeaderManager } from './managers/HeaderManager'; import { DateHeaderRenderer, type HeaderRenderer } from './renderers/HeaderRenderer'; import { DateColumnRenderer, type ColumnRenderer } from './renderers/ColumnRenderer'; import { DateEventRenderer, type EventRendererStrategy } from './renderers/EventRenderer'; +import { AllDayEventRenderer } from './renderers/AllDayEventRenderer'; import { GridRenderer } from './renderers/GridRenderer'; +import { NavigationRenderer } from './renderers/NavigationRenderer'; + +// Import utilities and services import { DateService } from './utils/DateService'; +import { TimeFormatter } from './utils/TimeFormatter'; +import { PositionUtils } from './utils/PositionUtils'; +import { AllDayLayoutEngine } from './utils/AllDayLayoutEngine'; +import { WorkHoursManager } from './managers/WorkHoursManager'; +import { GridStyleManager } from './renderers/GridStyleManager'; +import { EventStackManager } from './managers/EventStackManager'; +import { EventLayoutCoordinator } from './managers/EventLayoutCoordinator'; /** * Handle deep linking functionality after managers are initialized */ -async function handleDeepLinking(eventManager: EventManager): Promise { +async function handleDeepLinking(eventManager: EventManager, urlManager: URLManager): Promise { try { - const urlManager = new URLManager(eventBus); const eventId = urlManager.parseEventIdFromURL(); if (eventId) { @@ -80,8 +90,19 @@ async function initializeCalendar(): Promise { builder.registerType(DateColumnRenderer).as().keyed('resource'); builder.registerType(DateEventRenderer).as().keyed('resource'); + // Register core services and utilities builder.registerType(DateService).as().singleInstance(); - + builder.registerType(EventStackManager).as().singleInstance(); + builder.registerType(EventLayoutCoordinator).as().singleInstance(); + builder.registerType(GridStyleManager).as().singleInstance(); + builder.registerType(WorkHoursManager).as().singleInstance(); + builder.registerType(URLManager).as().singleInstance(); + builder.registerType(TimeFormatter).as().singleInstance(); + builder.registerType(PositionUtils).as().singleInstance(); + // Note: AllDayLayoutEngine is instantiated per-operation with specific dates, not a singleton + builder.registerType(NavigationRenderer).as().singleInstance(); + builder.registerType(AllDayEventRenderer).as().singleInstance(); + builder.registerType(EventRenderingService).as().singleInstance(); builder.registerType(GridRenderer).as().singleInstance(); builder.registerType(GridManager).as().singleInstance(); @@ -113,13 +134,14 @@ async function initializeCalendar(): Promise { const edgeScrollManager = app.resolveType(); const dragHoverManager = app.resolveType(); const allDayManager = app.resolveType(); + const urlManager = app.resolveType(); // Initialize managers await calendarManager.initialize?.(); await resizeHandleManager.initialize?.(); // Handle deep linking after managers are initialized - await handleDeepLinking(eventManager); + await handleDeepLinking(eventManager, urlManager); // Expose to window for debugging (with proper typing) (window as Window & { diff --git a/src/managers/AllDayManager.ts b/src/managers/AllDayManager.ts index dea9909..7c36981 100644 --- a/src/managers/AllDayManager.ts +++ b/src/managers/AllDayManager.ts @@ -1,7 +1,7 @@ // All-day row height management and animations import { eventBus } from '../core/EventBus'; -import { ALL_DAY_CONSTANTS, calendarConfig } from '../core/CalendarConfig'; +import { ALL_DAY_CONSTANTS } from '../core/CalendarConfig'; import { AllDayEventRenderer } from '../renderers/AllDayEventRenderer'; import { AllDayLayoutEngine, EventLayout } from '../utils/AllDayLayoutEngine'; import { ColumnBounds, ColumnDetectionUtils } from '../utils/ColumnDetectionUtils'; @@ -43,11 +43,14 @@ export class AllDayManager { private actualRowCount: number = 0; - constructor(eventManager: EventManager) { + constructor( + eventManager: EventManager, + allDayEventRenderer: AllDayEventRenderer, + dateService: DateService + ) { this.eventManager = eventManager; - this.allDayEventRenderer = new AllDayEventRenderer(); - const timezone = calendarConfig.getTimezone?.(); - this.dateService = new DateService(timezone); + this.allDayEventRenderer = allDayEventRenderer; + this.dateService = dateService; // Sync CSS variable with TypeScript constant to ensure consistency document.documentElement.style.setProperty('--single-row-height', `${ALL_DAY_CONSTANTS.EVENT_HEIGHT}px`); diff --git a/src/managers/CalendarManager.ts b/src/managers/CalendarManager.ts index 711aa0d..42f6154 100644 --- a/src/managers/CalendarManager.ts +++ b/src/managers/CalendarManager.ts @@ -1,5 +1,5 @@ import { CoreEvents } from '../constants/CoreEvents'; -import { calendarConfig } from '../core/CalendarConfig'; +import { CalendarConfig } from '../core/CalendarConfig'; import { CalendarView, IEventBus } from '../types/CalendarTypes'; import { EventManager } from './EventManager'; import { GridManager } from './GridManager'; @@ -8,7 +8,6 @@ import { ScrollManager } from './ScrollManager'; /** * CalendarManager - Main coordinator for all calendar managers - * Uses singleton calendarConfig for consistent configuration access */ export class CalendarManager { private eventBus: IEventBus; @@ -16,6 +15,7 @@ export class CalendarManager { private gridManager: GridManager; private eventRenderer: EventRenderingService; private scrollManager: ScrollManager; + private config: CalendarConfig; private currentView: CalendarView = 'week'; private currentDate: Date = new Date(); private isInitialized: boolean = false; @@ -25,14 +25,15 @@ export class CalendarManager { eventManager: EventManager, gridManager: GridManager, eventRenderingService: EventRenderingService, - scrollManager: ScrollManager + scrollManager: ScrollManager, + config: CalendarConfig ) { this.eventBus = eventBus; this.eventManager = eventManager; this.gridManager = gridManager; this.eventRenderer = eventRenderingService; this.scrollManager = scrollManager; - const timezone = calendarConfig.getTimezone?.(); + this.config = config; this.setupEventListeners(); } @@ -47,7 +48,7 @@ export class CalendarManager { try { // Debug: Check calendar type - const calendarType = calendarConfig.getCalendarMode(); + const calendarType = this.config.getCalendarMode(); // Step 1: Load data await this.eventManager.loadData(); @@ -212,7 +213,7 @@ export class CalendarManager { this.eventBus.emit('workweek:header-update', { currentDate: this.currentDate, currentView: this.currentView, - workweek: calendarConfig.getCurrentWorkWeek() + workweek: this.config.getCurrentWorkWeek() }); } diff --git a/src/managers/DragDropManager.ts b/src/managers/DragDropManager.ts index 138ad59..2298aa1 100644 --- a/src/managers/DragDropManager.ts +++ b/src/managers/DragDropManager.ts @@ -4,7 +4,6 @@ */ import { IEventBus } from '../types/CalendarTypes'; -import { calendarConfig } from '../core/CalendarConfig'; import { PositionUtils } from '../utils/PositionUtils'; import { ColumnBounds, ColumnDetectionUtils } from '../utils/ColumnDetectionUtils'; import { SwpEventElement, BaseSwpEventElement } from '../elements/SwpEventElement'; @@ -49,9 +48,11 @@ export class DragDropManager { private targetY = 0; private currentY = 0; private targetColumn: ColumnBounds | null = null; + private positionUtils: PositionUtils; - constructor(eventBus: IEventBus) { + constructor(eventBus: IEventBus, positionUtils: PositionUtils) { this.eventBus = eventBus; + this.positionUtils = positionUtils; this.init(); } @@ -415,7 +416,7 @@ export class DragDropManager { const eventTopY = mouseY - this.mouseOffset.y; // Snap the event top position, not the mouse position - const snappedY = PositionUtils.getPositionFromCoordinate(eventTopY, column); + const snappedY = this.positionUtils.getPositionFromCoordinate(eventTopY, column); return Math.max(0, snappedY); } diff --git a/src/managers/EventLayoutCoordinator.ts b/src/managers/EventLayoutCoordinator.ts index c653db7..55f565c 100644 --- a/src/managers/EventLayoutCoordinator.ts +++ b/src/managers/EventLayoutCoordinator.ts @@ -8,7 +8,7 @@ import { CalendarEvent } from '../types/CalendarTypes'; import { EventStackManager, EventGroup, StackLink } from './EventStackManager'; import { PositionUtils } from '../utils/PositionUtils'; -import { calendarConfig } from '../core/CalendarConfig'; +import { CalendarConfig } from '../core/CalendarConfig'; export interface GridGroupLayout { events: CalendarEvent[]; @@ -30,9 +30,13 @@ export interface ColumnLayout { export class EventLayoutCoordinator { private stackManager: EventStackManager; + private config: CalendarConfig; + private positionUtils: PositionUtils; - constructor() { - this.stackManager = new EventStackManager(); + constructor(stackManager: EventStackManager, config: CalendarConfig, positionUtils: PositionUtils) { + this.stackManager = stackManager; + this.config = config; + this.positionUtils = positionUtils; } /** @@ -55,7 +59,7 @@ export class EventLayoutCoordinator { // Find events that could be in GRID with first event // Use expanding search to find chains (A→B→C where each conflicts with next) - const gridSettings = calendarConfig.getGridSettings(); + const gridSettings = this.config.getGridSettings(); const thresholdMinutes = gridSettings.gridStartThresholdMinutes; // Use refactored method for expanding grid candidates @@ -78,7 +82,7 @@ export class EventLayoutCoordinator { // Ensure we get the earliest event (explicit sort for robustness) const earliestEvent = [...gridCandidates].sort((a, b) => a.start.getTime() - b.start.getTime())[0]; - const position = PositionUtils.calculateEventPosition(earliestEvent.start, earliestEvent.end); + const position = this.positionUtils.calculateEventPosition(earliestEvent.start, earliestEvent.end); const columns = this.allocateColumns(gridCandidates); gridGroupLayouts.push({ @@ -100,7 +104,7 @@ export class EventLayoutCoordinator { renderedEventsWithLevels ); - const position = PositionUtils.calculateEventPosition(firstEvent.start, firstEvent.end); + const position = this.positionUtils.calculateEventPosition(firstEvent.start, firstEvent.end); stackedEventLayouts.push({ event: firstEvent, stackLink: { stackLevel }, diff --git a/src/managers/EventManager.ts b/src/managers/EventManager.ts index 6b64107..66647ee 100644 --- a/src/managers/EventManager.ts +++ b/src/managers/EventManager.ts @@ -1,6 +1,6 @@ import { IEventBus, CalendarEvent, ResourceCalendarData } from '../types/CalendarTypes'; import { CoreEvents } from '../constants/CoreEvents'; -import { calendarConfig } from '../core/CalendarConfig'; +import { CalendarConfig } from '../core/CalendarConfig'; import { DateService } from '../utils/DateService'; import { ResourceData } from '../types/ManagerTypes'; @@ -20,16 +20,21 @@ interface RawEventData { * Handles data loading with improved performance and caching */ export class EventManager { - + private events: CalendarEvent[] = []; private rawData: ResourceCalendarData | RawEventData[] | null = null; private eventCache = new Map(); // Cache for period queries private lastCacheKey: string = ''; private dateService: DateService; + private config: CalendarConfig; - constructor(private eventBus: IEventBus) { - const timezone = calendarConfig.getTimezone?.(); - this.dateService = new DateService(timezone); + constructor( + private eventBus: IEventBus, + dateService: DateService, + config: CalendarConfig + ) { + this.dateService = dateService; + this.config = config; } /** @@ -50,7 +55,7 @@ export class EventManager { * Optimized mock data loading with better resource handling */ private async loadMockData(): Promise { - const calendarType = calendarConfig.getCalendarMode(); + const calendarType = this.config.getCalendarMode(); const jsonFile = calendarType === 'resource' ? '/src/data/mock-resource-events.json' : '/src/data/mock-events.json'; diff --git a/src/managers/EventStackManager.ts b/src/managers/EventStackManager.ts index 09a0e7c..da04ead 100644 --- a/src/managers/EventStackManager.ts +++ b/src/managers/EventStackManager.ts @@ -14,7 +14,7 @@ */ import { CalendarEvent } from '../types/CalendarTypes'; -import { calendarConfig } from '../core/CalendarConfig'; +import { CalendarConfig } from '../core/CalendarConfig'; export interface StackLink { prev?: string; // Event ID of previous event in stack @@ -30,6 +30,11 @@ export interface EventGroup { export class EventStackManager { private static readonly STACK_OFFSET_PX = 15; + private config: CalendarConfig; + + constructor(config: CalendarConfig) { + this.config = config; + } // ============================================ // PHASE 1: Start Time Grouping @@ -46,7 +51,7 @@ export class EventStackManager { if (events.length === 0) return []; // Get threshold from config - const gridSettings = calendarConfig.getGridSettings(); + const gridSettings = this.config.getGridSettings(); const thresholdMinutes = gridSettings.gridStartThresholdMinutes; // Sort events by start time diff --git a/src/managers/GridManager.ts b/src/managers/GridManager.ts index ca54f57..dbb417a 100644 --- a/src/managers/GridManager.ts +++ b/src/managers/GridManager.ts @@ -4,7 +4,6 @@ */ import { eventBus } from '../core/EventBus'; -import { calendarConfig } from '../core/CalendarConfig'; import { CoreEvents } from '../constants/CoreEvents'; import { ResourceCalendarData, CalendarView } from '../types/CalendarTypes'; import { GridRenderer } from '../renderers/GridRenderer'; @@ -23,11 +22,14 @@ export class GridManager { private styleManager: GridStyleManager; private dateService: DateService; - constructor(gridRenderer: GridRenderer) { - // Inject GridRenderer via DI + constructor( + gridRenderer: GridRenderer, + styleManager: GridStyleManager, + dateService: DateService + ) { this.gridRenderer = gridRenderer; - this.styleManager = new GridStyleManager(); - this.dateService = new DateService('Europe/Copenhagen'); + this.styleManager = styleManager; + this.dateService = dateService; this.init(); } diff --git a/src/managers/HeaderManager.ts b/src/managers/HeaderManager.ts index d42b73d..500ca0c 100644 --- a/src/managers/HeaderManager.ts +++ b/src/managers/HeaderManager.ts @@ -1,5 +1,5 @@ import { eventBus } from '../core/EventBus'; -import { calendarConfig } from '../core/CalendarConfig'; +import { CalendarConfig } from '../core/CalendarConfig'; import { CoreEvents } from '../constants/CoreEvents'; import { HeaderRenderer, HeaderRenderContext } from '../renderers/HeaderRenderer'; import { ResourceCalendarData } from '../types/CalendarTypes'; @@ -13,9 +13,11 @@ import { ColumnDetectionUtils } from '../utils/ColumnDetectionUtils'; */ export class HeaderManager { private headerRenderer: HeaderRenderer; + private config: CalendarConfig; - constructor(headerRenderer: HeaderRenderer) { + constructor(headerRenderer: HeaderRenderer, config: CalendarConfig) { this.headerRenderer = headerRenderer; + this.config = config; // Bind handler methods for event listeners this.handleDragMouseEnterHeader = this.handleDragMouseEnterHeader.bind(this); @@ -127,7 +129,7 @@ export class HeaderManager { // Render new header content using injected renderer const context: HeaderRenderContext = { currentWeek: currentDate, - config: calendarConfig, + config: this.config, resourceData: resourceData }; diff --git a/src/managers/NavigationManager.ts b/src/managers/NavigationManager.ts index e09912d..c7cdb56 100644 --- a/src/managers/NavigationManager.ts +++ b/src/managers/NavigationManager.ts @@ -4,7 +4,6 @@ import { DateService } from '../utils/DateService'; import { CoreEvents } from '../constants/CoreEvents'; import { NavigationRenderer } from '../renderers/NavigationRenderer'; import { GridRenderer } from '../renderers/GridRenderer'; -import { calendarConfig } from '../core/CalendarConfig'; /** * NavigationManager handles calendar navigation (prev/next/today buttons) @@ -19,10 +18,16 @@ export class NavigationManager { private targetWeek: Date; private animationQueue: number = 0; - constructor(eventBus: IEventBus, eventRenderer: EventRenderingService, gridRenderer: GridRenderer) { + constructor( + eventBus: IEventBus, + eventRenderer: EventRenderingService, + gridRenderer: GridRenderer, + dateService: DateService, + navigationRenderer: NavigationRenderer + ) { this.eventBus = eventBus; - this.dateService = new DateService('Europe/Copenhagen'); - this.navigationRenderer = new NavigationRenderer(eventBus, eventRenderer); + this.dateService = dateService; + this.navigationRenderer = navigationRenderer; this.gridRenderer = gridRenderer; this.currentWeek = this.getISOWeekStart(new Date()); this.targetWeek = new Date(this.currentWeek); diff --git a/src/managers/ResizeHandleManager.ts b/src/managers/ResizeHandleManager.ts index ead6ca3..e0b3f0a 100644 --- a/src/managers/ResizeHandleManager.ts +++ b/src/managers/ResizeHandleManager.ts @@ -1,6 +1,6 @@ import { eventBus } from '../core/EventBus'; import { CoreEvents } from '../constants/CoreEvents'; -import { calendarConfig } from '../core/CalendarConfig'; +import { CalendarConfig } from '../core/CalendarConfig'; import { ResizeEndEventPayload } from '../types/EventTypes'; type SwpEventEl = HTMLElement & { updateHeight?: (h: number) => void }; @@ -29,9 +29,11 @@ export class ResizeHandleManager { private unsubscribers: Array<() => void> = []; private pointerCaptured = false; private prevZ?: string; + private config: CalendarConfig; - constructor() { - const grid = calendarConfig.getGridSettings(); + constructor(config: CalendarConfig) { + this.config = config; + const grid = this.config.getGridSettings(); this.hourHeightPx = grid.hourHeight; this.snapMin = grid.snapInterval; this.minDurationMin = this.snapMin; // Use snap interval as minimum duration diff --git a/src/managers/ScrollManager.ts b/src/managers/ScrollManager.ts index 1f30107..518da9f 100644 --- a/src/managers/ScrollManager.ts +++ b/src/managers/ScrollManager.ts @@ -1,7 +1,6 @@ // Custom scroll management for calendar week container import { eventBus } from '../core/EventBus'; -import { calendarConfig } from '../core/CalendarConfig'; import { CoreEvents } from '../constants/CoreEvents'; import { PositionUtils } from '../utils/PositionUtils'; @@ -14,8 +13,10 @@ export class ScrollManager { private timeAxis: HTMLElement | null = null; private calendarHeader: HTMLElement | null = null; private resizeObserver: ResizeObserver | null = null; + private positionUtils: PositionUtils; - constructor() { + constructor(positionUtils: PositionUtils) { + this.positionUtils = positionUtils; this.init(); } @@ -112,7 +113,7 @@ export class ScrollManager { scrollToHour(hour: number): void { // Create time string for the hour const timeString = `${hour.toString().padStart(2, '0')}:00`; - const scrollTop = PositionUtils.timeToPixels(timeString); + const scrollTop = this.positionUtils.timeToPixels(timeString); this.scrollTo(scrollTop); } diff --git a/src/managers/ViewManager.ts b/src/managers/ViewManager.ts index 21cf563..969032c 100644 --- a/src/managers/ViewManager.ts +++ b/src/managers/ViewManager.ts @@ -1,6 +1,6 @@ import { EventBus } from '../core/EventBus'; import { CalendarView, IEventBus } from '../types/CalendarTypes'; -import { calendarConfig } from '../core/CalendarConfig'; +import { CalendarConfig } from '../core/CalendarConfig'; import { CoreEvents } from '../constants/CoreEvents'; /** @@ -9,17 +9,19 @@ import { CoreEvents } from '../constants/CoreEvents'; */ export class ViewManager { private eventBus: IEventBus; + private config: CalendarConfig; private currentView: CalendarView = 'week'; private buttonListeners: Map = new Map(); - + // Cached DOM elements for performance private cachedViewButtons: NodeListOf | null = null; private cachedWorkweekButtons: NodeListOf | null = null; private lastButtonCacheTime: number = 0; private readonly CACHE_DURATION = 5000; // 5 seconds - constructor(eventBus: IEventBus) { + constructor(eventBus: IEventBus, config: CalendarConfig) { this.eventBus = eventBus; + this.config = config; this.setupEventListeners(); } @@ -140,13 +142,13 @@ export class ViewManager { */ private changeWorkweek(workweekId: string): void { // Update the calendar config (does not emit events) - calendarConfig.setWorkWeek(workweekId); + this.config.setWorkWeek(workweekId); // Update button states using cached elements this.updateAllButtons(); // Emit workweek change event with full payload - const settings = calendarConfig.getWorkWeekSettings(); + const settings = this.config.getWorkWeekSettings(); this.eventBus.emit(CoreEvents.WORKWEEK_CHANGED, { workWeekId: workweekId, settings: settings @@ -166,7 +168,7 @@ export class ViewManager { this.updateButtonGroup( this.getWorkweekButtons(), 'data-workweek', - calendarConfig.getCurrentWorkWeek() + this.config.getCurrentWorkWeek() ); } diff --git a/src/managers/WorkHoursManager.ts b/src/managers/WorkHoursManager.ts index f9d8f9b..a76cfec 100644 --- a/src/managers/WorkHoursManager.ts +++ b/src/managers/WorkHoursManager.ts @@ -1,7 +1,7 @@ // Work hours management for per-column scheduling import { DateService } from '../utils/DateService'; -import { calendarConfig } from '../core/CalendarConfig'; +import { CalendarConfig } from '../core/CalendarConfig'; import { PositionUtils } from '../utils/PositionUtils'; /** @@ -35,11 +35,14 @@ export interface WorkScheduleConfig { */ export class WorkHoursManager { private dateService: DateService; + private config: CalendarConfig; + private positionUtils: PositionUtils; private workSchedule: WorkScheduleConfig; - constructor() { - const timezone = calendarConfig.getTimezone?.(); - this.dateService = new DateService(timezone); + constructor(dateService: DateService, config: CalendarConfig, positionUtils: PositionUtils) { + this.dateService = dateService; + this.config = config; + this.positionUtils = positionUtils; // Default work schedule - will be loaded from JSON later this.workSchedule = { @@ -98,8 +101,8 @@ export class WorkHoursManager { if (workHours === 'off') { return null; // Full day will be colored via CSS background } - - const gridSettings = calendarConfig.getGridSettings(); + + const gridSettings = this.config.getGridSettings(); const dayStartHour = gridSettings.dayStartHour; const hourHeight = gridSettings.hourHeight; @@ -128,7 +131,7 @@ export class WorkHoursManager { const endTime = `${workHours.end.toString().padStart(2, '0')}:00`; // Use PositionUtils for consistent position calculation - const position = PositionUtils.calculateEventPosition(startTime, endTime); + const position = this.positionUtils.calculateEventPosition(startTime, endTime); return { top: position.top, height: position.height }; } diff --git a/src/renderers/ColumnRenderer.ts b/src/renderers/ColumnRenderer.ts index 12b7dc6..f6ed64e 100644 --- a/src/renderers/ColumnRenderer.ts +++ b/src/renderers/ColumnRenderer.ts @@ -25,17 +25,20 @@ export interface ColumnRenderContext { * Date-based column renderer (original functionality) */ export class DateColumnRenderer implements ColumnRenderer { - private dateService!: DateService; - private workHoursManager!: WorkHoursManager; + private dateService: DateService; + private workHoursManager: WorkHoursManager; + + constructor( + dateService: DateService, + workHoursManager: WorkHoursManager + ) { + this.dateService = dateService; + this.workHoursManager = workHoursManager; + } render(columnContainer: HTMLElement, context: ColumnRenderContext): void { const { currentWeek, config } = context; - // Initialize date service and work hours manager - const timezone = config.getTimezone?.() || 'Europe/Copenhagen'; - this.dateService = new DateService(timezone); - this.workHoursManager = new WorkHoursManager(); - const workWeekSettings = config.getWorkWeekSettings(); const dates = this.dateService.getWorkWeekDates(currentWeek, workWeekSettings.workDays); const dateSettings = config.getDateViewSettings(); diff --git a/src/renderers/EventRenderer.ts b/src/renderers/EventRenderer.ts index 9e995d1..3a84582 100644 --- a/src/renderers/EventRenderer.ts +++ b/src/renderers/EventRenderer.ts @@ -1,7 +1,7 @@ // Event rendering strategy interface and implementations import { CalendarEvent } from '../types/CalendarTypes'; -import { calendarConfig } from '../core/CalendarConfig'; +import { CalendarConfig } from '../core/CalendarConfig'; import { SwpEventElement } from '../elements/SwpEventElement'; import { PositionUtils } from '../utils/PositionUtils'; import { ColumnBounds } from '../utils/ColumnDetectionUtils'; @@ -34,14 +34,23 @@ export class DateEventRenderer implements EventRendererStrategy { private dateService: DateService; private stackManager: EventStackManager; private layoutCoordinator: EventLayoutCoordinator; + private config: CalendarConfig; + private positionUtils: PositionUtils; private draggedClone: HTMLElement | null = null; private originalEvent: HTMLElement | null = null; - constructor() { - const timezone = calendarConfig.getTimezone?.(); - this.dateService = new DateService(timezone); - this.stackManager = new EventStackManager(); - this.layoutCoordinator = new EventLayoutCoordinator(); + constructor( + dateService: DateService, + stackManager: EventStackManager, + layoutCoordinator: EventLayoutCoordinator, + config: CalendarConfig, + positionUtils: PositionUtils + ) { + this.dateService = dateService; + this.stackManager = stackManager; + this.layoutCoordinator = layoutCoordinator; + this.config = config; + this.positionUtils = positionUtils; } private applyDragStyling(element: HTMLElement): void { @@ -303,7 +312,7 @@ export class DateEventRenderer implements EventRendererStrategy { // (e.g., if container starts at 07:00 and event starts at 08:15, offset = 75 min) const timeDiffMs = event.start.getTime() - containerStart.getTime(); const timeDiffMinutes = timeDiffMs / (1000 * 60); - const gridSettings = calendarConfig.getGridSettings(); + const gridSettings = this.config.getGridSettings(); const relativeTop = timeDiffMinutes > 0 ? (timeDiffMinutes / 60) * gridSettings.hourHeight : 0; // Events in grid columns are positioned absolutely within their column container @@ -333,7 +342,7 @@ export class DateEventRenderer implements EventRendererStrategy { protected calculateEventPosition(event: CalendarEvent): { top: number; height: number } { // Delegate to PositionUtils for centralized position calculation - return PositionUtils.calculateEventPosition(event.start, event.end); + return this.positionUtils.calculateEventPosition(event.start, event.end); } clearEvents(container?: HTMLElement): void { diff --git a/src/renderers/EventRendererManager.ts b/src/renderers/EventRendererManager.ts index 237fd20..9b93582 100644 --- a/src/renderers/EventRendererManager.ts +++ b/src/renderers/EventRendererManager.ts @@ -1,7 +1,6 @@ import { EventBus } from '../core/EventBus'; import { IEventBus, CalendarEvent, RenderContext } from '../types/CalendarTypes'; import { CoreEvents } from '../constants/CoreEvents'; -import { calendarConfig } from '../core/CalendarConfig'; import { EventManager } from '../managers/EventManager'; import { EventRendererStrategy } from './EventRenderer'; import { SwpEventElement } from '../elements/SwpEventElement'; @@ -20,14 +19,16 @@ export class EventRenderingService { private dragMouseLeaveHeaderListener: ((event: Event) => void) | null = null; - constructor(eventBus: IEventBus, eventManager: EventManager, strategy: EventRendererStrategy) { + constructor( + eventBus: IEventBus, + eventManager: EventManager, + strategy: EventRendererStrategy, + dateService: DateService + ) { this.eventBus = eventBus; this.eventManager = eventManager; this.strategy = strategy; - - // Initialize DateService - const timezone = calendarConfig.getTimezone?.(); - this.dateService = new DateService(timezone); + this.dateService = dateService; this.setupEventListeners(); } diff --git a/src/renderers/GridRenderer.ts b/src/renderers/GridRenderer.ts index b7bda6f..2bac0d3 100644 --- a/src/renderers/GridRenderer.ts +++ b/src/renderers/GridRenderer.ts @@ -1,4 +1,4 @@ -import { calendarConfig } from '../core/CalendarConfig'; +import { CalendarConfig } from '../core/CalendarConfig'; import { ResourceCalendarData, CalendarView } from '../types/CalendarTypes'; import { ColumnRenderer, ColumnRenderContext } from './ColumnRenderer'; import { eventBus } from '../core/EventBus'; @@ -15,11 +15,16 @@ export class GridRenderer { private cachedTimeAxis: HTMLElement | null = null; private dateService: DateService; private columnRenderer: ColumnRenderer; + private config: CalendarConfig; - constructor(columnRenderer: ColumnRenderer) { - const timezone = calendarConfig.getTimezone?.(); - this.dateService = new DateService(timezone); + constructor( + columnRenderer: ColumnRenderer, + dateService: DateService, + config: CalendarConfig + ) { + this.dateService = dateService; this.columnRenderer = columnRenderer; + this.config = config; } public renderGrid( @@ -80,7 +85,7 @@ export class GridRenderer { private createOptimizedTimeAxis(): HTMLElement { const timeAxis = document.createElement('swp-time-axis'); const timeAxisContent = document.createElement('swp-time-axis-content'); - const gridSettings = calendarConfig.getGridSettings(); + const gridSettings = this.config.getGridSettings(); const startHour = gridSettings.dayStartHour; const endHour = gridSettings.dayEndHour; @@ -142,7 +147,7 @@ export class GridRenderer { ): void { const context: ColumnRenderContext = { currentWeek: currentDate, // ColumnRenderer expects currentWeek property - config: calendarConfig, + config: this.config, resourceData: resourceData }; diff --git a/src/renderers/GridStyleManager.ts b/src/renderers/GridStyleManager.ts index c53207c..9fd6759 100644 --- a/src/renderers/GridStyleManager.ts +++ b/src/renderers/GridStyleManager.ts @@ -1,4 +1,4 @@ -import { calendarConfig } from '../core/CalendarConfig'; +import { CalendarConfig } from '../core/CalendarConfig'; import { ResourceCalendarData } from '../types/CalendarTypes'; interface GridSettings { @@ -16,7 +16,10 @@ interface GridSettings { * Separated from GridManager to follow Single Responsibility Principle */ export class GridStyleManager { - constructor() { + private config: CalendarConfig; + + constructor(config: CalendarConfig) { + this.config = config; } /** @@ -24,9 +27,9 @@ export class GridStyleManager { */ public updateGridStyles(resourceData: ResourceCalendarData | null = null): void { const root = document.documentElement; - const gridSettings = calendarConfig.getGridSettings(); + const gridSettings = this.config.getGridSettings(); const calendar = document.querySelector('swp-calendar') as HTMLElement; - const calendarType = calendarConfig.getCalendarMode(); + const calendarType = this.config.getCalendarMode(); // Set CSS variables for time and grid measurements this.setTimeVariables(root, gridSettings); @@ -66,9 +69,9 @@ export class GridStyleManager { if (calendarType === 'resource' && resourceData) { return resourceData.resources.length; } else if (calendarType === 'date') { - const dateSettings = calendarConfig.getDateViewSettings(); - const workWeekSettings = calendarConfig.getWorkWeekSettings(); - + const dateSettings = this.config.getDateViewSettings(); + const workWeekSettings = this.config.getWorkWeekSettings(); + switch (dateSettings.period) { case 'day': return 1; @@ -80,8 +83,8 @@ export class GridStyleManager { return workWeekSettings.totalDays; } } - - return calendarConfig.getWorkWeekSettings().totalDays; // Default to work week + + return this.config.getWorkWeekSettings().totalDays; // Default to work week } /** diff --git a/src/renderers/HeaderRenderer.ts b/src/renderers/HeaderRenderer.ts index 25a86fa..61b418d 100644 --- a/src/renderers/HeaderRenderer.ts +++ b/src/renderers/HeaderRenderer.ts @@ -37,7 +37,7 @@ export class DateHeaderRenderer implements HeaderRenderer { // Initialize date service with timezone and locale from config const timezone = config.getTimezone?.() || 'Europe/Copenhagen'; const locale = config.getLocale?.() || 'da-DK'; - this.dateService = new DateService(timezone); + this.dateService = new DateService(config); const workWeekSettings = config.getWorkWeekSettings(); const dates = this.dateService.getWorkWeekDates(currentWeek, workWeekSettings.workDays); diff --git a/src/utils/PositionUtils.ts b/src/utils/PositionUtils.ts index 15a546d..a13ac76 100644 --- a/src/utils/PositionUtils.ts +++ b/src/utils/PositionUtils.ts @@ -1,22 +1,28 @@ -import { calendarConfig } from '../core/CalendarConfig'; +import { CalendarConfig } from '../core/CalendarConfig'; import { ColumnBounds } from './ColumnDetectionUtils'; import { DateService } from './DateService'; import { TimeFormatter } from './TimeFormatter'; /** - * PositionUtils - Static positioning utilities using singleton calendarConfig + * PositionUtils - Positioning utilities with dependency injection * Focuses on pixel/position calculations while delegating date operations * * Note: Uses DateService with date-fns for all date/time operations */ export class PositionUtils { - private static dateService = new DateService('Europe/Copenhagen'); - + private dateService: DateService; + private config: CalendarConfig; + + constructor(dateService: DateService, config: CalendarConfig) { + this.dateService = dateService; + this.config = config; + } + /** * Convert minutes to pixels */ - public static minutesToPixels(minutes: number): number { - const gridSettings = calendarConfig.getGridSettings(); + public minutesToPixels(minutes: number): number { + const gridSettings = this.config.getGridSettings(); const pixelsPerHour = gridSettings.hourHeight; return (minutes / 60) * pixelsPerHour; } @@ -24,8 +30,8 @@ export class PositionUtils { /** * Convert pixels to minutes */ - public static pixelsToMinutes(pixels: number): number { - const gridSettings = calendarConfig.getGridSettings(); + public pixelsToMinutes(pixels: number): number { + const gridSettings = this.config.getGridSettings(); const pixelsPerHour = gridSettings.hourHeight; return (pixels / pixelsPerHour) * 60; } @@ -33,43 +39,43 @@ export class PositionUtils { /** * Convert time (HH:MM) to pixels from day start using DateService */ - public static timeToPixels(timeString: string): number { - const totalMinutes = PositionUtils.dateService.timeToMinutes(timeString); - const gridSettings = calendarConfig.getGridSettings(); + public timeToPixels(timeString: string): number { + const totalMinutes = this.dateService.timeToMinutes(timeString); + const gridSettings = this.config.getGridSettings(); const dayStartMinutes = gridSettings.dayStartHour * 60; const minutesFromDayStart = totalMinutes - dayStartMinutes; - - return PositionUtils.minutesToPixels(minutesFromDayStart); + + return this.minutesToPixels(minutesFromDayStart); } /** * Convert Date object to pixels from day start using DateService */ - public static dateToPixels(date: Date): number { - const totalMinutes = PositionUtils.dateService.getMinutesSinceMidnight(date); - const gridSettings = calendarConfig.getGridSettings(); + public dateToPixels(date: Date): number { + const totalMinutes = this.dateService.getMinutesSinceMidnight(date); + const gridSettings = this.config.getGridSettings(); const dayStartMinutes = gridSettings.dayStartHour * 60; const minutesFromDayStart = totalMinutes - dayStartMinutes; - - return PositionUtils.minutesToPixels(minutesFromDayStart); + + return this.minutesToPixels(minutesFromDayStart); } /** * Convert pixels to time using DateService */ - public static pixelsToTime(pixels: number): string { - const minutes = PositionUtils.pixelsToMinutes(pixels); - const gridSettings = calendarConfig.getGridSettings(); + public pixelsToTime(pixels: number): string { + const minutes = this.pixelsToMinutes(pixels); + const gridSettings = this.config.getGridSettings(); const dayStartMinutes = gridSettings.dayStartHour * 60; const totalMinutes = dayStartMinutes + minutes; - - return PositionUtils.dateService.minutesToTime(totalMinutes); + + return this.dateService.minutesToTime(totalMinutes); } /** * Beregn event position og størrelse */ - public static calculateEventPosition(startTime: string | Date, endTime: string | Date): { + public calculateEventPosition(startTime: string | Date, endTime: string | Date): { top: number; height: number; duration: number; @@ -78,19 +84,19 @@ export class PositionUtils { let endPixels: number; if (typeof startTime === 'string') { - startPixels = PositionUtils.timeToPixels(startTime); + startPixels = this.timeToPixels(startTime); } else { - startPixels = PositionUtils.dateToPixels(startTime); + startPixels = this.dateToPixels(startTime); } if (typeof endTime === 'string') { - endPixels = PositionUtils.timeToPixels(endTime); + endPixels = this.timeToPixels(endTime); } else { - endPixels = PositionUtils.dateToPixels(endTime); + endPixels = this.dateToPixels(endTime); } - const height = Math.max(endPixels - startPixels, PositionUtils.getMinimumEventHeight()); - const duration = PositionUtils.pixelsToMinutes(height); + const height = Math.max(endPixels - startPixels, this.getMinimumEventHeight()); + const duration = this.pixelsToMinutes(height); return { top: startPixels, @@ -102,40 +108,40 @@ export class PositionUtils { /** * Snap position til grid interval */ - public static snapToGrid(pixels: number): number { - const gridSettings = calendarConfig.getGridSettings(); + public snapToGrid(pixels: number): number { + const gridSettings = this.config.getGridSettings(); const snapInterval = gridSettings.snapInterval; - const snapPixels = PositionUtils.minutesToPixels(snapInterval); - + const snapPixels = this.minutesToPixels(snapInterval); + return Math.round(pixels / snapPixels) * snapPixels; } /** * Snap time to interval using DateService */ - public static snapTimeToInterval(timeString: string): string { - const totalMinutes = PositionUtils.dateService.timeToMinutes(timeString); - const gridSettings = calendarConfig.getGridSettings(); + public snapTimeToInterval(timeString: string): string { + const totalMinutes = this.dateService.timeToMinutes(timeString); + const gridSettings = this.config.getGridSettings(); const snapInterval = gridSettings.snapInterval; - + const snappedMinutes = Math.round(totalMinutes / snapInterval) * snapInterval; - return PositionUtils.dateService.minutesToTime(snappedMinutes); + return this.dateService.minutesToTime(snappedMinutes); } /** * Beregn kolonne position for overlappende events */ - public static calculateColumnPosition(eventIndex: number, totalColumns: number, containerWidth: number): { + public calculateColumnPosition(eventIndex: number, totalColumns: number, containerWidth: number): { left: number; width: number; } { const columnWidth = containerWidth / totalColumns; const left = eventIndex * columnWidth; - + // Lav lidt margin mellem kolonnerne const margin = 2; const adjustedWidth = columnWidth - margin; - + return { left: left + (margin / 2), width: Math.max(adjustedWidth, 50) // Minimum width @@ -145,63 +151,63 @@ export class PositionUtils { /** * Check om to events overlapper i tid */ - public static eventsOverlap( + public eventsOverlap( start1: string | Date, end1: string | Date, start2: string | Date, end2: string | Date ): boolean { - const pos1 = PositionUtils.calculateEventPosition(start1, end1); - const pos2 = PositionUtils.calculateEventPosition(start2, end2); - + const pos1 = this.calculateEventPosition(start1, end1); + const pos2 = this.calculateEventPosition(start2, end2); + const event1End = pos1.top + pos1.height; const event2End = pos2.top + pos2.height; - + return !(event1End <= pos2.top || event2End <= pos1.top); } /** * Beregn Y position fra mouse/touch koordinat */ - public static getPositionFromCoordinate(clientY: number, column: ColumnBounds): number { - + public getPositionFromCoordinate(clientY: number, column: ColumnBounds): number { + const relativeY = clientY - column.boundingClientRect.top; - + // Snap til grid - return PositionUtils.snapToGrid(relativeY); + return this.snapToGrid(relativeY); } /** * Valider at tid er inden for arbejdstimer */ - public static isWithinWorkHours(timeString: string): boolean { + public isWithinWorkHours(timeString: string): boolean { const [hours] = timeString.split(':').map(Number); - const gridSettings = calendarConfig.getGridSettings(); + const gridSettings = this.config.getGridSettings(); return hours >= gridSettings.workStartHour && hours < gridSettings.workEndHour; } /** * Valider at tid er inden for dag grænser */ - public static isWithinDayBounds(timeString: string): boolean { + public isWithinDayBounds(timeString: string): boolean { const [hours] = timeString.split(':').map(Number); - const gridSettings = calendarConfig.getGridSettings(); + const gridSettings = this.config.getGridSettings(); return hours >= gridSettings.dayStartHour && hours < gridSettings.dayEndHour; } /** * Hent minimum event højde i pixels */ - public static getMinimumEventHeight(): number { + public getMinimumEventHeight(): number { // Minimum 15 minutter - return PositionUtils.minutesToPixels(15); + return this.minutesToPixels(15); } /** * Hent maksimum event højde i pixels (hele dagen) */ - public static getMaximumEventHeight(): number { - const gridSettings = calendarConfig.getGridSettings(); + public getMaximumEventHeight(): number { + const gridSettings = this.config.getGridSettings(); const dayDurationHours = gridSettings.dayEndHour - gridSettings.dayStartHour; return dayDurationHours * gridSettings.hourHeight; } @@ -209,14 +215,14 @@ export class PositionUtils { /** * Beregn total kalender højde */ - public static getTotalCalendarHeight(): number { - return PositionUtils.getMaximumEventHeight(); + public getTotalCalendarHeight(): number { + return this.getMaximumEventHeight(); } /** * Convert ISO datetime to time string with UTC-to-local conversion */ - public static isoToTimeString(isoString: string): string { + public isoToTimeString(isoString: string): string { const date = new Date(isoString); return TimeFormatter.formatTime(date); } @@ -224,34 +230,34 @@ export class PositionUtils { /** * Convert time string to ISO datetime using DateService with timezone handling */ - public static timeStringToIso(timeString: string, date: Date = new Date()): string { - const totalMinutes = PositionUtils.dateService.timeToMinutes(timeString); - const newDate = PositionUtils.dateService.createDateAtTime(date, totalMinutes); - return PositionUtils.dateService.toUTC(newDate); + public timeStringToIso(timeString: string, date: Date = new Date()): string { + const totalMinutes = this.dateService.timeToMinutes(timeString); + const newDate = this.dateService.createDateAtTime(date, totalMinutes); + return this.dateService.toUTC(newDate); } /** * Calculate event duration using DateService */ - public static calculateDuration(startTime: string | Date, endTime: string | Date): number { - return PositionUtils.dateService.getDurationMinutes(startTime, endTime); + public calculateDuration(startTime: string | Date, endTime: string | Date): number { + return this.dateService.getDurationMinutes(startTime, endTime); } /** * Format duration to readable text (Danish) */ - public static formatDuration(minutes: number): string { + public formatDuration(minutes: number): string { if (minutes < 60) { return `${minutes} min`; } - + const hours = Math.floor(minutes / 60); const remainingMinutes = minutes % 60; - + if (remainingMinutes === 0) { return `${hours} time${hours !== 1 ? 'r' : ''}`; } - + return `${hours}t ${remainingMinutes}m`; } } \ No newline at end of file diff --git a/src/utils/TimeFormatter.ts b/src/utils/TimeFormatter.ts index 735b5dc..fe55171 100644 --- a/src/utils/TimeFormatter.ts +++ b/src/utils/TimeFormatter.ts @@ -27,74 +27,52 @@ export class TimeFormatter { showSeconds: false // Don't show seconds by default }; - private static dateService: DateService = new DateService('Europe/Copenhagen'); + // DateService will be initialized lazily to avoid circular dependency with CalendarConfig + private static dateService: DateService | null = null; + + private static getDateService(): DateService { + if (!TimeFormatter.dateService) { + // Create a minimal config object for DateService + const config = { + getTimezone: () => TimeFormatter.settings.timezone + }; + TimeFormatter.dateService = new DateService(config as any); + } + return TimeFormatter.dateService; + } /** * Configure time formatting settings */ static configure(settings: Partial): void { TimeFormatter.settings = { ...TimeFormatter.settings, ...settings }; - // Update DateService with new timezone - TimeFormatter.dateService = new DateService(TimeFormatter.settings.timezone); + // Reset DateService to pick up new timezone + TimeFormatter.dateService = null; } /** - * Get current time format settings - */ - static getSettings(): TimeFormatSettings { - return { ...TimeFormatter.settings }; - } - - /** - * Convert UTC date to configured timezone + * Convert UTC date to configured timezone (internal helper) * @param utcDate - Date in UTC (or ISO string) * @returns Date object adjusted to configured timezone */ - static convertToLocalTime(utcDate: Date | string): Date { + private static convertToLocalTime(utcDate: Date | string): Date { if (typeof utcDate === 'string') { - return TimeFormatter.dateService.fromUTC(utcDate); + return TimeFormatter.getDateService().fromUTC(utcDate); } - + // If it's already a Date object, convert to UTC string first, then back to local const utcString = utcDate.toISOString(); - return TimeFormatter.dateService.fromUTC(utcString); + return TimeFormatter.getDateService().fromUTC(utcString); } /** - * Get timezone offset for configured timezone - * @param date - Reference date for calculating offset (handles DST) - * @returns Offset in minutes - */ - static getTimezoneOffset(date: Date = new Date()): number { - const utc = new Date(date.getTime() + (date.getTimezoneOffset() * 60000)); - const targetTime = new Date(utc.toLocaleString('en-US', { timeZone: TimeFormatter.settings.timezone })); - return (targetTime.getTime() - utc.getTime()) / 60000; - } - - /** - * Format time in 12-hour format - * @param date - Date to format - * @returns Formatted time string (e.g., "9:00 AM") - */ - static format12Hour(date: Date): string { - const localDate = TimeFormatter.convertToLocalTime(date); - - return localDate.toLocaleTimeString(TimeFormatter.settings.locale, { - timeZone: TimeFormatter.settings.timezone, - hour: 'numeric', - minute: '2-digit', - hour12: true - }); - } - - /** - * Format time in 24-hour format using DateService + * Format time in 24-hour format using DateService (internal helper) * @param date - Date to format * @returns Formatted time string (e.g., "09:00") */ - static format24Hour(date: Date): string { + private static format24Hour(date: Date): string { const localDate = TimeFormatter.convertToLocalTime(date); - return TimeFormatter.dateService.formatTime(localDate, TimeFormatter.settings.showSeconds); + return TimeFormatter.getDateService().formatTime(localDate, TimeFormatter.settings.showSeconds); } /** @@ -103,38 +81,8 @@ export class TimeFormatter { * @returns Formatted time string */ static formatTime(date: Date): string { - return TimeFormatter.settings.use24HourFormat - ? TimeFormatter.format24Hour(date) - : TimeFormatter.format12Hour(date); - } - - /** - * Format time from total minutes since midnight using DateService - * @param totalMinutes - Minutes since midnight (e.g., 540 for 9:00 AM) - * @returns Formatted time string - */ - static formatTimeFromMinutes(totalMinutes: number): string { - return TimeFormatter.dateService.formatTimeFromMinutes(totalMinutes); - } - - /** - * Format date and time together - * @param date - Date to format - * @returns Formatted date and time string - */ - static formatDateTime(date: Date): string { - const localDate = TimeFormatter.convertToLocalTime(date); - - const dateStr = localDate.toLocaleDateString(TimeFormatter.settings.locale, { - timeZone: TimeFormatter.settings.timezone, - year: 'numeric', - month: '2-digit', - day: '2-digit' - }); - - const timeStr = TimeFormatter.formatTime(date); - - return `${dateStr} ${timeStr}`; + // Always use 24-hour format (12-hour support removed as unused) + return TimeFormatter.format24Hour(date); } /** @@ -146,77 +94,6 @@ export class TimeFormatter { static formatTimeRange(startDate: Date, endDate: Date): string { const localStart = TimeFormatter.convertToLocalTime(startDate); const localEnd = TimeFormatter.convertToLocalTime(endDate); - return TimeFormatter.dateService.formatTimeRange(localStart, localEnd); - } - - /** - * Check if current timezone observes daylight saving time - * @param date - Reference date - * @returns True if DST is active - */ - static isDaylightSavingTime(date: Date = new Date()): boolean { - const january = new Date(date.getFullYear(), 0, 1); - const july = new Date(date.getFullYear(), 6, 1); - - const janOffset = TimeFormatter.getTimezoneOffset(january); - const julOffset = TimeFormatter.getTimezoneOffset(july); - - return Math.max(janOffset, julOffset) !== TimeFormatter.getTimezoneOffset(date); - } - - /** - * Get timezone abbreviation (e.g., "CET", "CEST") - * @param date - Reference date - * @returns Timezone abbreviation - */ - static getTimezoneAbbreviation(date: Date = new Date()): string { - const localDate = TimeFormatter.convertToLocalTime(date); - - return localDate.toLocaleTimeString('en-US', { - timeZone: TimeFormatter.settings.timezone, - timeZoneName: 'short' - }).split(' ').pop() || ''; - } - - /** - * Format date in technical format: yyyy-mm-dd using DateService - */ - static formatDateTechnical(date: Date): string { - const localDate = TimeFormatter.convertToLocalTime(date); - return TimeFormatter.dateService.formatDate(localDate); - } - - /** - * Format time in technical format: hh:mm or hh:mm:ss using DateService - */ - static formatTimeTechnical(date: Date, includeSeconds: boolean = false): string { - const localDate = TimeFormatter.convertToLocalTime(date); - return TimeFormatter.dateService.formatTime(localDate, includeSeconds); - } - - /** - * Format date and time in technical format: yyyy-mm-dd hh:mm:ss using DateService - */ - static formatDateTimeTechnical(date: Date): string { - const localDate = TimeFormatter.convertToLocalTime(date); - return TimeFormatter.dateService.formatTechnicalDateTime(localDate); - } - - /** - * Convert local date to UTC ISO string using DateService - * @param localDate - Date in local timezone - * @returns ISO string in UTC (with 'Z' suffix) - */ - static toUTC(localDate: Date): string { - return TimeFormatter.dateService.toUTC(localDate); - } - - /** - * Convert UTC ISO string to local date using DateService - * @param utcString - ISO string in UTC - * @returns Date in local timezone - */ - static fromUTC(utcString: string): Date { - return TimeFormatter.dateService.fromUTC(utcString); + return TimeFormatter.getDateService().formatTimeRange(localStart, localEnd); } } \ No newline at end of file diff --git a/test/managers/EventStackManager.flexbox.test.ts b/test/managers/EventStackManager.flexbox.test.ts index 0b3592c..068e49a 100644 --- a/test/managers/EventStackManager.flexbox.test.ts +++ b/test/managers/EventStackManager.flexbox.test.ts @@ -16,16 +16,20 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { EventStackManager } from '../../src/managers/EventStackManager'; import { EventLayoutCoordinator } from '../../src/managers/EventLayoutCoordinator'; -import { calendarConfig } from '../../src/core/CalendarConfig'; +import { CalendarConfig } from '../../src/core/CalendarConfig'; +import { PositionUtils } from '../../src/utils/PositionUtils'; +import { DateService } from '../../src/utils/DateService'; describe('EventStackManager - Flexbox & Nested Stacking (3-Phase Algorithm)', () => { let manager: EventStackManager; let thresholdMinutes: number; + let config: CalendarConfig; beforeEach(() => { - manager = new EventStackManager(); + config = new CalendarConfig(); + manager = new EventStackManager(config); // Get threshold from config - tests should work with any value - thresholdMinutes = calendarConfig.getGridSettings().gridStartThresholdMinutes; + thresholdMinutes = config.getGridSettings().gridStartThresholdMinutes; }); // ============================================ @@ -1128,7 +1132,9 @@ describe('EventStackManager - Flexbox & Nested Stacking (3-Phase Algorithm)', () ]; // Use EventLayoutCoordinator to test column allocation - const coordinator = new EventLayoutCoordinator(); + const dateService = new DateService(config); + const positionUtils = new PositionUtils(dateService, config); + const coordinator = new EventLayoutCoordinator(manager, config, positionUtils); if (thresholdMinutes >= 30) { // Calculate layout diff --git a/test/managers/NavigationManager.edge-cases.test.ts b/test/managers/NavigationManager.edge-cases.test.ts index 7010d15..b4024af 100644 --- a/test/managers/NavigationManager.edge-cases.test.ts +++ b/test/managers/NavigationManager.edge-cases.test.ts @@ -3,6 +3,7 @@ import { NavigationManager } from '../../src/managers/NavigationManager'; import { EventBus } from '../../src/core/EventBus'; import { EventRenderingService } from '../../src/renderers/EventRendererManager'; import { DateService } from '../../src/utils/DateService'; +import { CalendarConfig } from '../../src/core/CalendarConfig'; describe('NavigationManager - Edge Cases', () => { let navigationManager: NavigationManager; @@ -11,9 +12,12 @@ describe('NavigationManager - Edge Cases', () => { beforeEach(() => { eventBus = new EventBus(); + const config = new CalendarConfig(); + dateService = new DateService(config); const mockEventRenderer = {} as EventRenderingService; - navigationManager = new NavigationManager(eventBus, mockEventRenderer); - dateService = new DateService('Europe/Copenhagen'); + const mockGridRenderer = {} as any; + const mockNavigationRenderer = {} as any; + navigationManager = new NavigationManager(eventBus, mockEventRenderer, mockGridRenderer, dateService, mockNavigationRenderer); }); describe('Week 53 Navigation', () => { diff --git a/test/utils/DateService.edge-cases.test.ts b/test/utils/DateService.edge-cases.test.ts index da50aa2..ce96fe5 100644 --- a/test/utils/DateService.edge-cases.test.ts +++ b/test/utils/DateService.edge-cases.test.ts @@ -1,8 +1,10 @@ import { describe, it, expect } from 'vitest'; import { DateService } from '../../src/utils/DateService'; +import { CalendarConfig } from '../../src/core/CalendarConfig'; describe('DateService - Edge Cases', () => { - const dateService = new DateService('Europe/Copenhagen'); + const config = new CalendarConfig(); + const dateService = new DateService(config); describe('Leap Year Handling', () => { it('should handle February 29 in leap year (2024)', () => { diff --git a/test/utils/DateService.test.ts b/test/utils/DateService.test.ts index 4944c81..69013ac 100644 --- a/test/utils/DateService.test.ts +++ b/test/utils/DateService.test.ts @@ -1,11 +1,13 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { DateService } from '../../src/utils/DateService'; +import { CalendarConfig } from '../../src/core/CalendarConfig'; describe('DateService', () => { let dateService: DateService; beforeEach(() => { - dateService = new DateService('Europe/Copenhagen'); + const config = new CalendarConfig(); + dateService = new DateService(config); }); describe('Core Conversions', () => { diff --git a/test/utils/DateService.validation.test.ts b/test/utils/DateService.validation.test.ts index 9cf41b8..d4031c5 100644 --- a/test/utils/DateService.validation.test.ts +++ b/test/utils/DateService.validation.test.ts @@ -1,8 +1,10 @@ import { describe, it, expect } from 'vitest'; import { DateService } from '../../src/utils/DateService'; +import { CalendarConfig } from '../../src/core/CalendarConfig'; describe('DateService - Validation', () => { - const dateService = new DateService('Europe/Copenhagen'); + const config = new CalendarConfig(); + const dateService = new DateService(config); describe('isValid() - Basic Date Validation', () => { it('should validate normal dates', () => { diff --git a/test/utils/TimeFormatter.test.ts b/test/utils/TimeFormatter.test.ts index d4c705c..102a168 100644 --- a/test/utils/TimeFormatter.test.ts +++ b/test/utils/TimeFormatter.test.ts @@ -13,274 +13,99 @@ describe('TimeFormatter', () => { }); }); - describe('UTC to Local Time Conversion', () => { - it('should convert UTC time to Copenhagen time (winter time, UTC+1)', () => { - // January 15, 2025 10:00:00 UTC = 11:00:00 CET (UTC+1) - let utcDate = new Date('2025-01-15T10:00:00Z'); - let localDate = TimeFormatter.convertToLocalTime(utcDate); - - let hours = localDate.getHours(); - let expectedHours = 11; - - expect(hours).toBe(expectedHours); - }); - - it('should convert UTC time to Copenhagen time (summer time, UTC+2)', () => { - // July 15, 2025 10:00:00 UTC = 12:00:00 CEST (UTC+2) - let utcDate = new Date('2025-07-15T10:00:00Z'); - let localDate = TimeFormatter.convertToLocalTime(utcDate); - - let hours = localDate.getHours(); - let expectedHours = 12; - - expect(hours).toBe(expectedHours); - }); - - it('should handle midnight UTC correctly in winter', () => { - // January 15, 2025 00:00:00 UTC = 01:00:00 CET - let utcDate = new Date('2025-01-15T00:00:00Z'); - let localDate = TimeFormatter.convertToLocalTime(utcDate); - - let hours = localDate.getHours(); - let expectedHours = 1; - - expect(hours).toBe(expectedHours); - }); - - it('should handle midnight UTC correctly in summer', () => { - // July 15, 2025 00:00:00 UTC = 02:00:00 CEST - let utcDate = new Date('2025-07-15T00:00:00Z'); - let localDate = TimeFormatter.convertToLocalTime(utcDate); - - let hours = localDate.getHours(); - let expectedHours = 2; - - expect(hours).toBe(expectedHours); - }); - - it('should handle date crossing midnight when converting from UTC', () => { - // January 14, 2025 23:30:00 UTC = January 15, 2025 00:30:00 CET - let utcDate = new Date('2025-01-14T23:30:00Z'); - let localDate = TimeFormatter.convertToLocalTime(utcDate); - - let day = localDate.getDate(); - let hours = localDate.getHours(); - let minutes = localDate.getMinutes(); - - let expectedDay = 15; - let expectedHours = 0; - let expectedMinutes = 30; - - expect(day).toBe(expectedDay); - expect(hours).toBe(expectedHours); - expect(minutes).toBe(expectedMinutes); - }); - }); - describe('Time Formatting', () => { it('should format time in 24-hour format', () => { let date = new Date('2025-01-15T10:30:00Z'); - let formatted = TimeFormatter.format24Hour(date); - + let formatted = TimeFormatter.formatTime(date); + // Should be 11:30 in Copenhagen (UTC+1 in winter) - // Always use colon separator expect(formatted).toBe('11:30'); }); - it('should format time in 12-hour format', () => { - let date = new Date('2025-01-15T13:30:00Z'); - let formatted = TimeFormatter.format12Hour(date); - - // Should be 2:30 PM in Copenhagen (14:30 CET = 2:30 PM) - // 12-hour format can use locale formatting with AM/PM - // Note: locale may use dot separator and space: "2.30 PM" - expect(formatted).toMatch(/2[.:\s]+30/); - expect(formatted).toMatch(/PM/i); - }); + it('should format time at midnight', () => { + let date = new Date('2025-01-15T23:00:00Z'); + let formatted = TimeFormatter.formatTime(date); - it('should format time from minutes correctly', () => { - // 540 minutes = 9:00 AM - let formatted = TimeFormatter.formatTimeFromMinutes(540); - - // Always use colon separator - expect(formatted).toBe('09:00'); + // Should be 00:00 next day in Copenhagen + expect(formatted).toBe('00:00'); }); it('should format time range correctly', () => { let startDate = new Date('2025-01-15T08:00:00Z'); let endDate = new Date('2025-01-15T10:00:00Z'); let formatted = TimeFormatter.formatTimeRange(startDate, endDate); - + // 08:00 UTC = 09:00 CET, 10:00 UTC = 11:00 CET - // Always use colon separator expect(formatted).toBe('09:00 - 11:00'); }); - }); - describe('Technical Date/Time Formatting', () => { - it('should format date in technical format yyyy-mm-dd', () => { - let date = new Date('2025-01-15T10:00:00Z'); - let formatted = TimeFormatter.formatDateTechnical(date); - - expect(formatted).toMatch(/2025-01-15/); - }); + it('should format time range across midnight', () => { + let startDate = new Date('2025-01-15T22:00:00Z'); + let endDate = new Date('2025-01-16T01:00:00Z'); + let formatted = TimeFormatter.formatTimeRange(startDate, endDate); - it('should format time in technical format hh:mm', () => { - let date = new Date('2025-01-15T08:30:00Z'); - let formatted = TimeFormatter.formatTimeTechnical(date, false); - - // 08:30 UTC = 09:30 CET - expect(formatted).toMatch(/09:30/); - }); - - it('should format time in technical format hh:mm:ss when includeSeconds is true', () => { - let date = new Date('2025-01-15T08:30:45Z'); - let formatted = TimeFormatter.formatTimeTechnical(date, true); - - // 08:30:45 UTC = 09:30:45 CET - expect(formatted).toMatch(/09:30:45/); - }); - - it('should format datetime in technical format yyyy-mm-dd hh:mm', () => { - TimeFormatter.configure({ showSeconds: false }); - let date = new Date('2025-01-15T08:30:00Z'); - let formatted = TimeFormatter.formatDateTimeTechnical(date); - - // 08:30 UTC = 09:30 CET on same day - expect(formatted).toMatch(/2025-01-15 09:30/); - }); - - it('should format datetime with seconds when configured', () => { - TimeFormatter.configure({ showSeconds: true }); - let date = new Date('2025-01-15T08:30:45Z'); - let formatted = TimeFormatter.formatDateTimeTechnical(date); - - expect(formatted).toMatch(/2025-01-15 09:30:45/); - }); - }); - - describe('Timezone Information', () => { - it('should detect daylight saving time correctly', () => { - let winterDate = new Date('2025-01-15T12:00:00Z'); - let summerDate = new Date('2025-07-15T12:00:00Z'); - - let isWinterDST = TimeFormatter.isDaylightSavingTime(winterDate); - let isSummerDST = TimeFormatter.isDaylightSavingTime(summerDate); - - // Copenhagen: Winter = no DST (CET), Summer = DST (CEST) - // Note: The implementation might not work correctly in all environments - // Skip this test for now as DST detection is complex - expect(isWinterDST).toBe(false); - // Summer DST detection may vary by environment - expect(typeof isSummerDST).toBe('boolean'); - }); - - it('should get correct timezone abbreviation', () => { - let winterDate = new Date('2025-01-15T12:00:00Z'); - let summerDate = new Date('2025-07-15T12:00:00Z'); - - let winterAbbr = TimeFormatter.getTimezoneAbbreviation(winterDate); - let summerAbbr = TimeFormatter.getTimezoneAbbreviation(summerDate); - - // Copenhagen uses CET in winter, CEST in summer - expect(winterAbbr).toMatch(/CET|GMT\+1/); - expect(summerAbbr).toMatch(/CEST|GMT\+2/); + // 22:00 UTC = 23:00 CET, 01:00 UTC = 02:00 CET next day + expect(formatted).toBe('23:00 - 02:00'); }); }); describe('Configuration', () => { - it('should use configured timezone', () => { - // Note: convertToLocalTime doesn't actually use the configured timezone - // It just converts UTC to browser's local time - // This is a limitation of the current implementation + it('should respect timezone configuration', () => { TimeFormatter.configure({ timezone: 'America/New_York' }); - - let utcDate = new Date('2025-01-15T10:00:00Z'); - let localDate = TimeFormatter.convertToLocalTime(utcDate); - - // The conversion happens but timezone config isn't used in convertToLocalTime - // Just verify it returns a valid date - expect(localDate).toBeInstanceOf(Date); - expect(localDate.getTime()).toBeGreaterThan(0); + + let date = new Date('2025-01-15T10:00:00Z'); + let formatted = TimeFormatter.formatTime(date); + + // 10:00 UTC = 05:00 EST (UTC-5 in winter) + expect(formatted).toBe('05:00'); }); - it('should respect 24-hour format setting', () => { - TimeFormatter.configure({ use24HourFormat: true }); - let date = new Date('2025-01-15T13:00:00Z'); - let formatted = TimeFormatter.formatTime(date); - - // Always use colon separator for 24-hour format - expect(formatted).toBe('14:00'); // 14:00 CET - }); + it('should respect showSeconds configuration', () => { + TimeFormatter.configure({ showSeconds: true }); - it('should respect 12-hour format setting', () => { - TimeFormatter.configure({ use24HourFormat: false }); - let date = new Date('2025-01-15T13:00:00Z'); + let date = new Date('2025-01-15T10:30:45Z'); let formatted = TimeFormatter.formatTime(date); - - // 12-hour format can use locale formatting with AM/PM - // Note: locale may use dot separator and space: "2.00 PM" - expect(formatted).toMatch(/2[.:\s]+00/); // 2:00 PM CET - expect(formatted).toMatch(/PM/i); + + // Should include seconds + expect(formatted).toBe('11:30:45'); }); }); describe('Edge Cases', () => { it('should handle DST transition correctly (spring forward)', () => { // March 30, 2025 01:00:00 UTC is when Copenhagen springs forward - // 01:00 UTC = 02:00 CET, but at 02:00 CET clocks jump to 03:00 CEST let beforeDST = new Date('2025-03-30T00:59:00Z'); let afterDST = new Date('2025-03-30T01:01:00Z'); - - let beforeLocal = TimeFormatter.convertToLocalTime(beforeDST); - let afterLocal = TimeFormatter.convertToLocalTime(afterDST); - - let beforeHours = beforeLocal.getHours(); - let afterHours = afterLocal.getHours(); - + + let beforeFormatted = TimeFormatter.formatTime(beforeDST); + let afterFormatted = TimeFormatter.formatTime(afterDST); + // Before: 00:59 UTC = 01:59 CET // After: 01:01 UTC = 03:01 CEST (jumped from 02:00 to 03:00) - expect(beforeHours).toBe(1); - expect(afterHours).toBe(3); + expect(beforeFormatted).toBe('01:59'); + expect(afterFormatted).toBe('03:01'); }); it('should handle DST transition correctly (fall back)', () => { // October 26, 2025 01:00:00 UTC is when Copenhagen falls back - // 01:00 UTC = 03:00 CEST, but at 03:00 CEST clocks fall back to 02:00 CET let beforeDST = new Date('2025-10-26T00:59:00Z'); let afterDST = new Date('2025-10-26T01:01:00Z'); - - let beforeLocal = TimeFormatter.convertToLocalTime(beforeDST); - let afterLocal = TimeFormatter.convertToLocalTime(afterDST); - - let beforeHours = beforeLocal.getHours(); - let afterHours = afterLocal.getHours(); - + + let beforeFormatted = TimeFormatter.formatTime(beforeDST); + let afterFormatted = TimeFormatter.formatTime(afterDST); + // Before: 00:59 UTC = 02:59 CEST // After: 01:01 UTC = 02:01 CET (fell back from 03:00 to 02:00) - expect(beforeHours).toBe(2); - expect(afterHours).toBe(2); + expect(beforeFormatted).toBe('02:59'); + expect(afterFormatted).toBe('02:01'); }); it('should handle year boundary correctly', () => { // December 31, 2024 23:30:00 UTC = January 1, 2025 00:30:00 CET - let utcDate = new Date('2024-12-31T23:30:00Z'); - let localDate = TimeFormatter.convertToLocalTime(utcDate); - - let year = localDate.getFullYear(); - let month = localDate.getMonth(); - let day = localDate.getDate(); - let hours = localDate.getHours(); - - let expectedYear = 2025; - let expectedMonth = 0; // January - let expectedDay = 1; - let expectedHours = 0; - - expect(year).toBe(expectedYear); - expect(month).toBe(expectedMonth); - expect(day).toBe(expectedDay); - expect(hours).toBe(expectedHours); + let date = new Date('2024-12-31T23:30:00Z'); + let formatted = TimeFormatter.formatTime(date); + + expect(formatted).toBe('00:30'); }); }); -}); \ No newline at end of file +});