2025-09-23 20:44:15 +02:00
|
|
|
import { calendarConfig } from '../core/CalendarConfig';
|
2025-09-28 13:25:09 +02:00
|
|
|
import { ColumnBounds } from './ColumnDetectionUtils';
|
2025-10-03 20:50:40 +02:00
|
|
|
import { DateService } from './DateService';
|
2025-10-03 16:05:22 +02:00
|
|
|
import { TimeFormatter } from './TimeFormatter';
|
2025-07-24 22:17:38 +02:00
|
|
|
|
|
|
|
|
/**
|
2025-09-03 20:04:47 +02:00
|
|
|
* PositionUtils - Static positioning utilities using singleton calendarConfig
|
2025-09-03 19:05:03 +02:00
|
|
|
* Focuses on pixel/position calculations while delegating date operations
|
2025-10-03 16:47:42 +02:00
|
|
|
*
|
2025-10-03 20:50:40 +02:00
|
|
|
* Note: Uses DateService with date-fns for all date/time operations
|
2025-07-24 22:17:38 +02:00
|
|
|
*/
|
|
|
|
|
export class PositionUtils {
|
2025-10-03 20:50:40 +02:00
|
|
|
private static dateService = new DateService('Europe/Copenhagen');
|
|
|
|
|
|
2025-07-24 22:17:38 +02:00
|
|
|
/**
|
2025-08-09 00:31:44 +02:00
|
|
|
* Convert minutes to pixels
|
2025-07-24 22:17:38 +02:00
|
|
|
*/
|
2025-09-03 20:04:47 +02:00
|
|
|
public static minutesToPixels(minutes: number): number {
|
|
|
|
|
const gridSettings = calendarConfig.getGridSettings();
|
2025-08-09 00:31:44 +02:00
|
|
|
const pixelsPerHour = gridSettings.hourHeight;
|
2025-07-24 22:17:38 +02:00
|
|
|
return (minutes / 60) * pixelsPerHour;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-08-09 00:31:44 +02:00
|
|
|
* Convert pixels to minutes
|
2025-07-24 22:17:38 +02:00
|
|
|
*/
|
2025-09-03 20:04:47 +02:00
|
|
|
public static pixelsToMinutes(pixels: number): number {
|
|
|
|
|
const gridSettings = calendarConfig.getGridSettings();
|
2025-08-09 00:31:44 +02:00
|
|
|
const pixelsPerHour = gridSettings.hourHeight;
|
2025-07-24 22:17:38 +02:00
|
|
|
return (pixels / pixelsPerHour) * 60;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-03 20:50:40 +02:00
|
|
|
* Convert time (HH:MM) to pixels from day start using DateService
|
2025-07-24 22:17:38 +02:00
|
|
|
*/
|
2025-09-03 20:04:47 +02:00
|
|
|
public static timeToPixels(timeString: string): number {
|
2025-10-03 20:50:40 +02:00
|
|
|
const totalMinutes = PositionUtils.dateService.timeToMinutes(timeString);
|
2025-09-03 20:04:47 +02:00
|
|
|
const gridSettings = calendarConfig.getGridSettings();
|
2025-08-09 00:31:44 +02:00
|
|
|
const dayStartMinutes = gridSettings.dayStartHour * 60;
|
2025-07-24 22:17:38 +02:00
|
|
|
const minutesFromDayStart = totalMinutes - dayStartMinutes;
|
|
|
|
|
|
2025-09-03 20:04:47 +02:00
|
|
|
return PositionUtils.minutesToPixels(minutesFromDayStart);
|
2025-07-24 22:17:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-03 20:50:40 +02:00
|
|
|
* Convert Date object to pixels from day start using DateService
|
2025-07-24 22:17:38 +02:00
|
|
|
*/
|
2025-09-03 20:04:47 +02:00
|
|
|
public static dateToPixels(date: Date): number {
|
2025-10-03 20:50:40 +02:00
|
|
|
const totalMinutes = PositionUtils.dateService.getMinutesSinceMidnight(date);
|
2025-09-03 20:04:47 +02:00
|
|
|
const gridSettings = calendarConfig.getGridSettings();
|
2025-08-09 00:31:44 +02:00
|
|
|
const dayStartMinutes = gridSettings.dayStartHour * 60;
|
2025-07-24 22:17:38 +02:00
|
|
|
const minutesFromDayStart = totalMinutes - dayStartMinutes;
|
|
|
|
|
|
2025-09-03 20:04:47 +02:00
|
|
|
return PositionUtils.minutesToPixels(minutesFromDayStart);
|
2025-07-24 22:17:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-03 20:50:40 +02:00
|
|
|
* Convert pixels to time using DateService
|
2025-07-24 22:17:38 +02:00
|
|
|
*/
|
2025-09-03 20:04:47 +02:00
|
|
|
public static pixelsToTime(pixels: number): string {
|
|
|
|
|
const minutes = PositionUtils.pixelsToMinutes(pixels);
|
|
|
|
|
const gridSettings = calendarConfig.getGridSettings();
|
2025-08-09 00:31:44 +02:00
|
|
|
const dayStartMinutes = gridSettings.dayStartHour * 60;
|
2025-07-24 22:17:38 +02:00
|
|
|
const totalMinutes = dayStartMinutes + minutes;
|
|
|
|
|
|
2025-10-03 20:50:40 +02:00
|
|
|
return PositionUtils.dateService.minutesToTime(totalMinutes);
|
2025-07-24 22:17:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Beregn event position og størrelse
|
|
|
|
|
*/
|
2025-09-03 20:04:47 +02:00
|
|
|
public static calculateEventPosition(startTime: string | Date, endTime: string | Date): {
|
2025-07-24 22:17:38 +02:00
|
|
|
top: number;
|
|
|
|
|
height: number;
|
|
|
|
|
duration: number;
|
|
|
|
|
} {
|
|
|
|
|
let startPixels: number;
|
|
|
|
|
let endPixels: number;
|
|
|
|
|
|
|
|
|
|
if (typeof startTime === 'string') {
|
2025-09-03 20:04:47 +02:00
|
|
|
startPixels = PositionUtils.timeToPixels(startTime);
|
2025-07-24 22:17:38 +02:00
|
|
|
} else {
|
2025-09-03 20:04:47 +02:00
|
|
|
startPixels = PositionUtils.dateToPixels(startTime);
|
2025-07-24 22:17:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (typeof endTime === 'string') {
|
2025-09-03 20:04:47 +02:00
|
|
|
endPixels = PositionUtils.timeToPixels(endTime);
|
2025-07-24 22:17:38 +02:00
|
|
|
} else {
|
2025-09-03 20:04:47 +02:00
|
|
|
endPixels = PositionUtils.dateToPixels(endTime);
|
2025-07-24 22:17:38 +02:00
|
|
|
}
|
|
|
|
|
|
2025-09-03 20:04:47 +02:00
|
|
|
const height = Math.max(endPixels - startPixels, PositionUtils.getMinimumEventHeight());
|
|
|
|
|
const duration = PositionUtils.pixelsToMinutes(height);
|
2025-07-24 22:17:38 +02:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
top: startPixels,
|
|
|
|
|
height,
|
|
|
|
|
duration
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Snap position til grid interval
|
|
|
|
|
*/
|
2025-09-03 20:04:47 +02:00
|
|
|
public static snapToGrid(pixels: number): number {
|
|
|
|
|
const gridSettings = calendarConfig.getGridSettings();
|
2025-08-09 00:31:44 +02:00
|
|
|
const snapInterval = gridSettings.snapInterval;
|
2025-09-03 20:04:47 +02:00
|
|
|
const snapPixels = PositionUtils.minutesToPixels(snapInterval);
|
2025-07-24 22:17:38 +02:00
|
|
|
|
|
|
|
|
return Math.round(pixels / snapPixels) * snapPixels;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-03 20:50:40 +02:00
|
|
|
* Snap time to interval using DateService
|
2025-07-24 22:17:38 +02:00
|
|
|
*/
|
2025-09-03 20:04:47 +02:00
|
|
|
public static snapTimeToInterval(timeString: string): string {
|
2025-10-03 20:50:40 +02:00
|
|
|
const totalMinutes = PositionUtils.dateService.timeToMinutes(timeString);
|
2025-09-03 20:04:47 +02:00
|
|
|
const gridSettings = calendarConfig.getGridSettings();
|
2025-08-09 00:31:44 +02:00
|
|
|
const snapInterval = gridSettings.snapInterval;
|
2025-07-24 22:17:38 +02:00
|
|
|
|
|
|
|
|
const snappedMinutes = Math.round(totalMinutes / snapInterval) * snapInterval;
|
2025-10-03 20:50:40 +02:00
|
|
|
return PositionUtils.dateService.minutesToTime(snappedMinutes);
|
2025-07-24 22:17:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Beregn kolonne position for overlappende events
|
|
|
|
|
*/
|
2025-09-03 20:04:47 +02:00
|
|
|
public static calculateColumnPosition(eventIndex: number, totalColumns: number, containerWidth: number): {
|
2025-07-24 22:17:38 +02:00
|
|
|
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
|
|
|
|
|
*/
|
2025-09-03 20:04:47 +02:00
|
|
|
public static eventsOverlap(
|
|
|
|
|
start1: string | Date,
|
|
|
|
|
end1: string | Date,
|
|
|
|
|
start2: string | Date,
|
2025-07-24 22:17:38 +02:00
|
|
|
end2: string | Date
|
|
|
|
|
): boolean {
|
2025-09-03 20:04:47 +02:00
|
|
|
const pos1 = PositionUtils.calculateEventPosition(start1, end1);
|
|
|
|
|
const pos2 = PositionUtils.calculateEventPosition(start2, end2);
|
2025-07-24 22:17:38 +02:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
*/
|
2025-09-28 13:25:09 +02:00
|
|
|
public static getPositionFromCoordinate(clientY: number, column: ColumnBounds): number {
|
|
|
|
|
|
|
|
|
|
const relativeY = clientY - column.boundingClientRect.top;
|
2025-07-24 22:17:38 +02:00
|
|
|
|
|
|
|
|
// Snap til grid
|
2025-09-03 20:04:47 +02:00
|
|
|
return PositionUtils.snapToGrid(relativeY);
|
2025-07-24 22:17:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Valider at tid er inden for arbejdstimer
|
|
|
|
|
*/
|
2025-09-03 20:04:47 +02:00
|
|
|
public static isWithinWorkHours(timeString: string): boolean {
|
2025-07-24 22:17:38 +02:00
|
|
|
const [hours] = timeString.split(':').map(Number);
|
2025-09-03 20:04:47 +02:00
|
|
|
const gridSettings = calendarConfig.getGridSettings();
|
2025-08-09 00:31:44 +02:00
|
|
|
return hours >= gridSettings.workStartHour && hours < gridSettings.workEndHour;
|
2025-07-24 22:17:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Valider at tid er inden for dag grænser
|
|
|
|
|
*/
|
2025-09-03 20:04:47 +02:00
|
|
|
public static isWithinDayBounds(timeString: string): boolean {
|
2025-07-24 22:17:38 +02:00
|
|
|
const [hours] = timeString.split(':').map(Number);
|
2025-09-03 20:04:47 +02:00
|
|
|
const gridSettings = calendarConfig.getGridSettings();
|
2025-08-09 00:31:44 +02:00
|
|
|
return hours >= gridSettings.dayStartHour && hours < gridSettings.dayEndHour;
|
2025-07-24 22:17:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Hent minimum event højde i pixels
|
|
|
|
|
*/
|
2025-09-03 20:04:47 +02:00
|
|
|
public static getMinimumEventHeight(): number {
|
2025-07-24 22:17:38 +02:00
|
|
|
// Minimum 15 minutter
|
2025-09-03 20:04:47 +02:00
|
|
|
return PositionUtils.minutesToPixels(15);
|
2025-07-24 22:17:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Hent maksimum event højde i pixels (hele dagen)
|
|
|
|
|
*/
|
2025-09-03 20:04:47 +02:00
|
|
|
public static getMaximumEventHeight(): number {
|
|
|
|
|
const gridSettings = calendarConfig.getGridSettings();
|
2025-08-09 00:31:44 +02:00
|
|
|
const dayDurationHours = gridSettings.dayEndHour - gridSettings.dayStartHour;
|
|
|
|
|
return dayDurationHours * gridSettings.hourHeight;
|
2025-07-24 22:17:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Beregn total kalender højde
|
|
|
|
|
*/
|
2025-09-03 20:04:47 +02:00
|
|
|
public static getTotalCalendarHeight(): number {
|
|
|
|
|
return PositionUtils.getMaximumEventHeight();
|
2025-07-24 22:17:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-03 16:05:22 +02:00
|
|
|
* Convert ISO datetime to time string with UTC-to-local conversion
|
2025-07-24 22:17:38 +02:00
|
|
|
*/
|
2025-09-03 20:04:47 +02:00
|
|
|
public static isoToTimeString(isoString: string): string {
|
2025-07-24 22:17:38 +02:00
|
|
|
const date = new Date(isoString);
|
2025-10-03 16:05:22 +02:00
|
|
|
return TimeFormatter.formatTime(date);
|
2025-07-24 22:17:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-04 00:32:26 +02:00
|
|
|
* Convert time string to ISO datetime using DateService with timezone handling
|
2025-07-24 22:17:38 +02:00
|
|
|
*/
|
2025-09-03 20:04:47 +02:00
|
|
|
public static timeStringToIso(timeString: string, date: Date = new Date()): string {
|
2025-10-03 20:50:40 +02:00
|
|
|
const totalMinutes = PositionUtils.dateService.timeToMinutes(timeString);
|
2025-10-03 20:59:52 +02:00
|
|
|
const newDate = PositionUtils.dateService.createDateAtTime(date, totalMinutes);
|
2025-10-04 00:32:26 +02:00
|
|
|
return PositionUtils.dateService.toUTC(newDate);
|
2025-07-24 22:17:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-03 20:50:40 +02:00
|
|
|
* Calculate event duration using DateService
|
2025-07-24 22:17:38 +02:00
|
|
|
*/
|
2025-09-03 20:04:47 +02:00
|
|
|
public static calculateDuration(startTime: string | Date, endTime: string | Date): number {
|
2025-10-03 20:50:40 +02:00
|
|
|
return PositionUtils.dateService.getDurationMinutes(startTime, endTime);
|
2025-07-24 22:17:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-09-03 19:05:03 +02:00
|
|
|
* Format duration to readable text (Danish)
|
2025-07-24 22:17:38 +02:00
|
|
|
*/
|
2025-09-03 20:04:47 +02:00
|
|
|
public static formatDuration(minutes: number): string {
|
2025-07-24 22:17:38 +02:00
|
|
|
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`;
|
|
|
|
|
}
|
|
|
|
|
}
|