import { calendarConfig } from '../core/CalendarConfig'; import { DateCalculator } from './DateCalculator'; /** * PositionUtils - Static positioning utilities using singleton calendarConfig * Focuses on pixel/position calculations while delegating date operations */ export class PositionUtils { /** * Convert minutes to pixels */ public static minutesToPixels(minutes: number): number { const gridSettings = calendarConfig.getGridSettings(); const pixelsPerHour = gridSettings.hourHeight; return (minutes / 60) * pixelsPerHour; } /** * Convert pixels to minutes */ public static pixelsToMinutes(pixels: number): number { const gridSettings = calendarConfig.getGridSettings(); const pixelsPerHour = gridSettings.hourHeight; return (pixels / pixelsPerHour) * 60; } /** * Convert time (HH:MM) to pixels from day start using DateCalculator */ public static timeToPixels(timeString: string): number { const totalMinutes = DateCalculator.timeToMinutes(timeString); const gridSettings = calendarConfig.getGridSettings(); const dayStartMinutes = gridSettings.dayStartHour * 60; const minutesFromDayStart = totalMinutes - dayStartMinutes; return PositionUtils.minutesToPixels(minutesFromDayStart); } /** * Convert Date object to pixels from day start using DateCalculator */ public static dateToPixels(date: Date): number { const totalMinutes = DateCalculator.getMinutesSinceMidnight(date); const gridSettings = calendarConfig.getGridSettings(); const dayStartMinutes = gridSettings.dayStartHour * 60; const minutesFromDayStart = totalMinutes - dayStartMinutes; return PositionUtils.minutesToPixels(minutesFromDayStart); } /** * Convert pixels to time using DateCalculator */ public static pixelsToTime(pixels: number): string { const minutes = PositionUtils.pixelsToMinutes(pixels); const gridSettings = calendarConfig.getGridSettings(); const dayStartMinutes = gridSettings.dayStartHour * 60; const totalMinutes = dayStartMinutes + minutes; return DateCalculator.minutesToTime(totalMinutes); } /** * Beregn event position og størrelse */ public static calculateEventPosition(startTime: string | Date, endTime: string | Date): { top: number; height: number; duration: number; } { let startPixels: number; let endPixels: number; if (typeof startTime === 'string') { startPixels = PositionUtils.timeToPixels(startTime); } else { startPixels = PositionUtils.dateToPixels(startTime); } if (typeof endTime === 'string') { endPixels = PositionUtils.timeToPixels(endTime); } else { endPixels = PositionUtils.dateToPixels(endTime); } const height = Math.max(endPixels - startPixels, PositionUtils.getMinimumEventHeight()); const duration = PositionUtils.pixelsToMinutes(height); return { top: startPixels, height, duration }; } /** * Snap position til grid interval */ public static snapToGrid(pixels: number): number { const gridSettings = calendarConfig.getGridSettings(); const snapInterval = gridSettings.snapInterval; const snapPixels = PositionUtils.minutesToPixels(snapInterval); return Math.round(pixels / snapPixels) * snapPixels; } /** * Snap time to interval using DateCalculator */ public static snapTimeToInterval(timeString: string): string { const totalMinutes = DateCalculator.timeToMinutes(timeString); const gridSettings = calendarConfig.getGridSettings(); const snapInterval = gridSettings.snapInterval; const snappedMinutes = Math.round(totalMinutes / snapInterval) * snapInterval; return DateCalculator.minutesToTime(snappedMinutes); } /** * Beregn kolonne position for overlappende events */ public static 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 }; } /** * Check om to events overlapper i tid */ public static 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 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, containerElement: HTMLElement): number { const rect = containerElement.getBoundingClientRect(); const relativeY = clientY - rect.top; // Snap til grid return PositionUtils.snapToGrid(relativeY); } /** * Beregn tid fra mouse/touch koordinat */ public static getTimeFromCoordinate(clientY: number, containerElement: HTMLElement): string { const position = PositionUtils.getPositionFromCoordinate(clientY, containerElement); return PositionUtils.pixelsToTime(position); } /** * Valider at tid er inden for arbejdstimer */ public static isWithinWorkHours(timeString: string): boolean { const [hours] = timeString.split(':').map(Number); const gridSettings = calendarConfig.getGridSettings(); return hours >= gridSettings.workStartHour && hours < gridSettings.workEndHour; } /** * Valider at tid er inden for dag grænser */ public static isWithinDayBounds(timeString: string): boolean { const [hours] = timeString.split(':').map(Number); const gridSettings = calendarConfig.getGridSettings(); return hours >= gridSettings.dayStartHour && hours < gridSettings.dayEndHour; } /** * Hent minimum event højde i pixels */ public static getMinimumEventHeight(): number { // Minimum 15 minutter return PositionUtils.minutesToPixels(15); } /** * Hent maksimum event højde i pixels (hele dagen) */ public static getMaximumEventHeight(): number { const gridSettings = calendarConfig.getGridSettings(); const dayDurationHours = gridSettings.dayEndHour - gridSettings.dayStartHour; return dayDurationHours * gridSettings.hourHeight; } /** * Beregn total kalender højde */ public static getTotalCalendarHeight(): number { return PositionUtils.getMaximumEventHeight(); } /** * Convert ISO datetime to time string using DateCalculator */ public static isoToTimeString(isoString: string): string { const date = new Date(isoString); return DateCalculator.formatTime(date); } /** * Convert time string to ISO datetime using DateCalculator */ public static timeStringToIso(timeString: string, date: Date = new Date()): string { const totalMinutes = DateCalculator.timeToMinutes(timeString); const hours = Math.floor(totalMinutes / 60); const minutes = totalMinutes % 60; const newDate = new Date(date); newDate.setHours(hours, minutes, 0, 0); return newDate.toISOString(); } /** * Calculate event duration using DateCalculator */ public static calculateDuration(startTime: string | Date, endTime: string | Date): number { return DateCalculator.getDurationMinutes(startTime, endTime); } /** * Format duration to readable text (Danish) */ public static 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`; } }