import { CalendarConfig } from '../core/CalendarConfig.js'; /** * PositionUtils - Utility funktioner til pixel/minut konvertering * Håndterer positionering og størrelse beregninger for calendar events */ export class PositionUtils { private config: CalendarConfig; constructor(config: CalendarConfig) { this.config = config; } /** * Konverter minutter til pixels */ public minutesToPixels(minutes: number): number { const pixelsPerHour = this.config.get('hourHeight'); return (minutes / 60) * pixelsPerHour; } /** * Konverter pixels til minutter */ public pixelsToMinutes(pixels: number): number { const pixelsPerHour = this.config.get('hourHeight'); return (pixels / pixelsPerHour) * 60; } /** * Konverter tid (HH:MM) til pixels fra dag start */ public timeToPixels(timeString: string): number { const [hours, minutes] = timeString.split(':').map(Number); const totalMinutes = (hours * 60) + minutes; const dayStartMinutes = this.config.get('dayStartHour') * 60; const minutesFromDayStart = totalMinutes - dayStartMinutes; return this.minutesToPixels(minutesFromDayStart); } /** * Konverter Date object til pixels fra dag start */ public dateToPixels(date: Date): number { const hours = date.getHours(); const minutes = date.getMinutes(); const totalMinutes = (hours * 60) + minutes; const dayStartMinutes = this.config.get('dayStartHour') * 60; const minutesFromDayStart = totalMinutes - dayStartMinutes; return this.minutesToPixels(minutesFromDayStart); } /** * Konverter pixels til tid (HH:MM format) */ public pixelsToTime(pixels: number): string { const minutes = this.pixelsToMinutes(pixels); const dayStartMinutes = this.config.get('dayStartHour') * 60; const totalMinutes = dayStartMinutes + minutes; const hours = Math.floor(totalMinutes / 60); const mins = Math.round(totalMinutes % 60); return `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}`; } /** * Beregn event position og størrelse */ public calculateEventPosition(startTime: string | Date, endTime: string | Date): { top: number; height: number; duration: number; } { let startPixels: number; let endPixels: number; if (typeof startTime === 'string') { startPixels = this.timeToPixels(startTime); } else { startPixels = this.dateToPixels(startTime); } if (typeof endTime === 'string') { endPixels = this.timeToPixels(endTime); } else { endPixels = this.dateToPixels(endTime); } const height = Math.max(endPixels - startPixels, this.getMinimumEventHeight()); const duration = this.pixelsToMinutes(height); return { top: startPixels, height, duration }; } /** * Snap position til grid interval */ public snapToGrid(pixels: number): number { const snapInterval = this.config.get('snapInterval'); const snapPixels = this.minutesToPixels(snapInterval); return Math.round(pixels / snapPixels) * snapPixels; } /** * Snap tid til interval */ public snapTimeToInterval(timeString: string): string { const [hours, minutes] = timeString.split(':').map(Number); const totalMinutes = (hours * 60) + minutes; const snapInterval = this.config.get('snapInterval'); const snappedMinutes = Math.round(totalMinutes / snapInterval) * snapInterval; const snappedHours = Math.floor(snappedMinutes / 60); const remainingMinutes = snappedMinutes % 60; return `${snappedHours.toString().padStart(2, '0')}:${remainingMinutes.toString().padStart(2, '0')}`; } /** * Beregn kolonne position for overlappende events */ 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 }; } /** * Check om to events overlapper i tid */ public eventsOverlap( start1: string | Date, end1: string | Date, start2: string | Date, end2: string | Date ): boolean { 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 getPositionFromCoordinate(clientY: number, containerElement: HTMLElement): number { const rect = containerElement.getBoundingClientRect(); const relativeY = clientY - rect.top; // Snap til grid return this.snapToGrid(relativeY); } /** * Beregn tid fra mouse/touch koordinat */ public getTimeFromCoordinate(clientY: number, containerElement: HTMLElement): string { const position = this.getPositionFromCoordinate(clientY, containerElement); return this.pixelsToTime(position); } /** * Valider at tid er inden for arbejdstimer */ public isWithinWorkHours(timeString: string): boolean { const [hours] = timeString.split(':').map(Number); return hours >= this.config.get('workStartHour') && hours < this.config.get('workEndHour'); } /** * Valider at tid er inden for dag grænser */ public isWithinDayBounds(timeString: string): boolean { const [hours] = timeString.split(':').map(Number); return hours >= this.config.get('dayStartHour') && hours < this.config.get('dayEndHour'); } /** * Hent minimum event højde i pixels */ public getMinimumEventHeight(): number { // Minimum 15 minutter return this.minutesToPixels(15); } /** * Hent maksimum event højde i pixels (hele dagen) */ public getMaximumEventHeight(): number { const dayDurationHours = this.config.get('dayEndHour') - this.config.get('dayStartHour'); return dayDurationHours * this.config.get('hourHeight'); } /** * Beregn total kalender højde */ public getTotalCalendarHeight(): number { return this.getMaximumEventHeight(); } /** * Konverter ISO datetime til lokal tid string */ public isoToTimeString(isoString: string): string { const date = new Date(isoString); const hours = date.getHours(); const minutes = date.getMinutes(); return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`; } /** * Konverter lokal tid string til ISO datetime for i dag */ public timeStringToIso(timeString: string, date: Date = new Date()): string { const [hours, minutes] = timeString.split(':').map(Number); const newDate = new Date(date); newDate.setHours(hours, minutes, 0, 0); return newDate.toISOString(); } /** * Beregn event varighed i minutter */ public calculateDuration(startTime: string | Date, endTime: string | Date): number { let startMs: number; let endMs: number; if (typeof startTime === 'string') { startMs = new Date(startTime).getTime(); } else { startMs = startTime.getTime(); } if (typeof endTime === 'string') { endMs = new Date(endTime).getTime(); } else { endMs = endTime.getTime(); } return Math.round((endMs - startMs) / (1000 * 60)); // Minutter } /** * Format varighed til læsbar tekst */ 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`; } /** * Opdater konfiguration */ public updateConfig(newConfig: CalendarConfig): void { this.config = newConfig; } }