Calendar/src/utils/PositionUtils.ts

260 lines
8.5 KiB
TypeScript
Raw Normal View History

import { calendarConfig } from '../core/CalendarConfig';
import { ColumnBounds } from './ColumnDetectionUtils';
import { DateCalculator } from './DateCalculator';
import { TimeFormatter } from './TimeFormatter';
/**
* PositionUtils - Static positioning utilities using singleton calendarConfig
* Focuses on pixel/position calculations while delegating date operations
*
* Note: Uses DateCalculator and TimeFormatter which internally use DateService with date-fns
*/
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, column: ColumnBounds): number {
const relativeY = clientY - column.boundingClientRect.top;
// Snap til grid
return PositionUtils.snapToGrid(relativeY);
}
/**
* 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 with UTC-to-local conversion
*/
public static isoToTimeString(isoString: string): string {
const date = new Date(isoString);
return TimeFormatter.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`;
}
}