Some ignored filles was missing

This commit is contained in:
Janus C. H. Knudsen 2026-02-03 00:02:25 +01:00
parent 7db22245e2
commit fd5ab6bc0d
268 changed files with 31970 additions and 4 deletions

View file

@ -0,0 +1,42 @@
import { ICalendarEvent } from '../types/CalendarTypes';
export interface IEventLayout {
calenderEvent: ICalendarEvent;
gridArea: string;
startColumn: number;
endColumn: number;
row: number;
columnSpan: number;
}
export declare class AllDayLayoutEngine {
private weekDates;
private tracks;
constructor(weekDates: string[]);
/**
* Calculate layout for all events using clean day-based logic
*/
calculateLayout(events: ICalendarEvent[]): IEventLayout[];
/**
* Find available track for event spanning from startDay to endDay (0-based indices)
*/
private findAvailableTrack;
/**
* Check if track is available for the given day range (0-based indices)
*/
private isTrackAvailable;
/**
* Get start day index for event (1-based, 0 if not visible)
*/
private getEventStartDay;
/**
* Get end day index for event (1-based, 0 if not visible)
*/
private getEventEndDay;
/**
* Check if event is visible in the current date range
*/
private isEventVisible;
/**
* Format date to YYYY-MM-DD string using local date
*/
private formatDate;
}

View file

@ -0,0 +1,108 @@
export class AllDayLayoutEngine {
constructor(weekDates) {
this.weekDates = weekDates;
this.tracks = [];
}
/**
* Calculate layout for all events using clean day-based logic
*/
calculateLayout(events) {
let layouts = [];
// Reset tracks for new calculation
this.tracks = [new Array(this.weekDates.length).fill(false)];
// Filter to only visible events
const visibleEvents = events.filter(event => this.isEventVisible(event));
// Process events in input order (no sorting)
for (const event of visibleEvents) {
const startDay = this.getEventStartDay(event);
const endDay = this.getEventEndDay(event);
if (startDay > 0 && endDay > 0) {
const track = this.findAvailableTrack(startDay - 1, endDay - 1); // Convert to 0-based for tracks
// Mark days as occupied
for (let day = startDay - 1; day <= endDay - 1; day++) {
this.tracks[track][day] = true;
}
const layout = {
calenderEvent: event,
gridArea: `${track + 1} / ${startDay} / ${track + 2} / ${endDay + 1}`,
startColumn: startDay,
endColumn: endDay,
row: track + 1,
columnSpan: endDay - startDay + 1
};
layouts.push(layout);
}
}
return layouts;
}
/**
* Find available track for event spanning from startDay to endDay (0-based indices)
*/
findAvailableTrack(startDay, endDay) {
for (let trackIndex = 0; trackIndex < this.tracks.length; trackIndex++) {
if (this.isTrackAvailable(trackIndex, startDay, endDay)) {
return trackIndex;
}
}
// Create new track if none available
this.tracks.push(new Array(this.weekDates.length).fill(false));
return this.tracks.length - 1;
}
/**
* Check if track is available for the given day range (0-based indices)
*/
isTrackAvailable(trackIndex, startDay, endDay) {
for (let day = startDay; day <= endDay; day++) {
if (this.tracks[trackIndex][day]) {
return false;
}
}
return true;
}
/**
* Get start day index for event (1-based, 0 if not visible)
*/
getEventStartDay(event) {
const eventStartDate = this.formatDate(event.start);
const firstVisibleDate = this.weekDates[0];
// If event starts before visible range, clip to first visible day
const clippedStartDate = eventStartDate < firstVisibleDate ? firstVisibleDate : eventStartDate;
const dayIndex = this.weekDates.indexOf(clippedStartDate);
return dayIndex >= 0 ? dayIndex + 1 : 0;
}
/**
* Get end day index for event (1-based, 0 if not visible)
*/
getEventEndDay(event) {
const eventEndDate = this.formatDate(event.end);
const lastVisibleDate = this.weekDates[this.weekDates.length - 1];
// If event ends after visible range, clip to last visible day
const clippedEndDate = eventEndDate > lastVisibleDate ? lastVisibleDate : eventEndDate;
const dayIndex = this.weekDates.indexOf(clippedEndDate);
return dayIndex >= 0 ? dayIndex + 1 : 0;
}
/**
* Check if event is visible in the current date range
*/
isEventVisible(event) {
if (this.weekDates.length === 0)
return false;
const eventStartDate = this.formatDate(event.start);
const eventEndDate = this.formatDate(event.end);
const firstVisibleDate = this.weekDates[0];
const lastVisibleDate = this.weekDates[this.weekDates.length - 1];
// Event overlaps if it doesn't end before visible range starts
// AND doesn't start after visible range ends
return !(eventEndDate < firstVisibleDate || eventStartDate > lastVisibleDate);
}
/**
* Format date to YYYY-MM-DD string using local date
*/
formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
}
//# sourceMappingURL=AllDayLayoutEngine.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"AllDayLayoutEngine.js","sourceRoot":"","sources":["../../../src/utils/AllDayLayoutEngine.ts"],"names":[],"mappings":"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"}

View file

@ -0,0 +1,30 @@
/**
* ColumnDetectionUtils - Shared utility for column detection and caching
* Used by both DragDropManager and AllDayManager for consistent column detection
*/
import { IMousePosition } from "../types/DragDropTypes";
export interface IColumnBounds {
date: string;
left: number;
right: number;
boundingClientRect: DOMRect;
element: HTMLElement;
index: number;
}
export declare class ColumnDetectionUtils {
private static columnBoundsCache;
/**
* Update column bounds cache for coordinate-based column detection
*/
static updateColumnBoundsCache(): void;
/**
* Get column date from X coordinate using cached bounds
*/
static getColumnBounds(position: IMousePosition): IColumnBounds | null;
/**
* Get column bounds by Date
*/
static getColumnBoundsByDate(date: Date): IColumnBounds | null;
static getColumns(): IColumnBounds[];
static getHeaderColumns(): IColumnBounds[];
}

View file

@ -0,0 +1,87 @@
/**
* ColumnDetectionUtils - Shared utility for column detection and caching
* Used by both DragDropManager and AllDayManager for consistent column detection
*/
export class ColumnDetectionUtils {
/**
* Update column bounds cache for coordinate-based column detection
*/
static updateColumnBoundsCache() {
// Reset cache
this.columnBoundsCache = [];
// Find alle kolonner
const columns = document.querySelectorAll('swp-day-column');
let index = 1;
// Cache hver kolonnes x-grænser
columns.forEach(column => {
const rect = column.getBoundingClientRect();
const date = column.dataset.date;
if (date) {
this.columnBoundsCache.push({
boundingClientRect: rect,
element: column,
date,
left: rect.left,
right: rect.right,
index: index++
});
}
});
// Sorter efter x-position (fra venstre til højre)
this.columnBoundsCache.sort((a, b) => a.left - b.left);
}
/**
* Get column date from X coordinate using cached bounds
*/
static getColumnBounds(position) {
if (this.columnBoundsCache.length === 0) {
this.updateColumnBoundsCache();
}
// Find den kolonne hvor x-koordinaten er indenfor grænserne
let column = this.columnBoundsCache.find(col => position.x >= col.left && position.x <= col.right);
if (column)
return column;
return null;
}
/**
* Get column bounds by Date
*/
static getColumnBoundsByDate(date) {
if (this.columnBoundsCache.length === 0) {
this.updateColumnBoundsCache();
}
// Convert Date to YYYY-MM-DD format
let dateString = date.toISOString().split('T')[0];
// Find column that matches the date
let column = this.columnBoundsCache.find(col => col.date === dateString);
return column || null;
}
static getColumns() {
return [...this.columnBoundsCache];
}
static getHeaderColumns() {
let dayHeaders = [];
const dayColumns = document.querySelectorAll('swp-calendar-header swp-day-header');
let index = 1;
// Cache hver kolonnes x-grænser
dayColumns.forEach(column => {
const rect = column.getBoundingClientRect();
const date = column.dataset.date;
if (date) {
dayHeaders.push({
boundingClientRect: rect,
element: column,
date,
left: rect.left,
right: rect.right,
index: index++
});
}
});
// Sorter efter x-position (fra venstre til højre)
dayHeaders.sort((a, b) => a.left - b.left);
return dayHeaders;
}
}
ColumnDetectionUtils.columnBoundsCache = [];
//# sourceMappingURL=ColumnDetectionUtils.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"ColumnDetectionUtils.js","sourceRoot":"","sources":["../../../src/utils/ColumnDetectionUtils.ts"],"names":[],"mappings":"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"}

149
wwwroot/js/utils/DateCalculator.d.ts vendored Normal file
View file

@ -0,0 +1,149 @@
/**
* DateCalculator - Centralized date calculation logic for calendar
* Handles all date computations with proper week start handling
*/
import { CalendarConfig } from '../core/CalendarConfig';
export declare class DateCalculator {
private static config;
/**
* Initialize DateCalculator with configuration
* @param config - Calendar configuration
*/
static initialize(config: CalendarConfig): void;
/**
* Validate that a date is valid
* @param date - Date to validate
* @param methodName - Name of calling method for error messages
* @throws Error if date is invalid
*/
private static validateDate;
/**
* Get dates for work week using ISO 8601 day numbering (Monday=1, Sunday=7)
* @param weekStart - Any date in the week
* @returns Array of dates for the configured work days
*/
static getWorkWeekDates(weekStart: Date): Date[];
/**
* Get the start of the ISO week (Monday) for a given date
* @param date - Any date in the week
* @returns The Monday of the ISO week
*/
static getISOWeekStart(date: Date): Date;
/**
* Get the end of the ISO week for a given date
* @param date - Any date in the week
* @returns The end date of the ISO week (Sunday)
*/
static getWeekEnd(date: Date): Date;
/**
* Get week number for a date (ISO 8601)
* @param date - The date to get week number for
* @returns Week number (1-53)
*/
static getWeekNumber(date: Date): number;
/**
* Format a date range with customizable options
* @param start - Start date
* @param end - End date
* @param options - Formatting options
* @returns Formatted date range string
*/
static formatDateRange(start: Date, end: Date, options?: {
locale?: string;
month?: 'numeric' | '2-digit' | 'long' | 'short' | 'narrow';
day?: 'numeric' | '2-digit';
year?: 'numeric' | '2-digit';
}): string;
/**
* Format a date to ISO date string (YYYY-MM-DD)
* @param date - Date to format
* @returns ISO date string
*/
static formatISODate(date: Date): string;
/**
* Check if a date is today
* @param date - Date to check
* @returns True if the date is today
*/
static isToday(date: Date): boolean;
/**
* Add days to a date
* @param date - Base date
* @param days - Number of days to add (can be negative)
* @returns New date
*/
static addDays(date: Date, days: number): Date;
/**
* Add weeks to a date
* @param date - Base date
* @param weeks - Number of weeks to add (can be negative)
* @returns New date
*/
static addWeeks(date: Date, weeks: number): Date;
/**
* Get all dates in a week
* @param weekStart - Start of the week
* @returns Array of 7 dates for the full week
*/
static getFullWeekDates(weekStart: Date): Date[];
/**
* Get the day name for a date using Intl.DateTimeFormat
* @param date - Date to get day name for
* @param format - 'short' or 'long'
* @returns Day name
*/
static getDayName(date: Date, format?: 'short' | 'long'): string;
/**
* Format time to HH:MM
* @param date - Date to format
* @returns Time string
*/
static formatTime(date: Date): string;
/**
* Format time to 12-hour format
* @param date - Date to format
* @returns 12-hour time string
*/
static formatTime12(date: Date): string;
/**
* Convert minutes since midnight to time string
* @param minutes - Minutes since midnight
* @returns Time string
*/
static minutesToTime(minutes: number): string;
/**
* Convert time string to minutes since midnight
* @param timeStr - Time string
* @returns Minutes since midnight
*/
static timeToMinutes(timeStr: string): number;
/**
* Get minutes since start of day
* @param date - Date or ISO string
* @returns Minutes since midnight
*/
static getMinutesSinceMidnight(date: Date | string): number;
/**
* Calculate duration in minutes between two dates
* @param start - Start date or ISO string
* @param end - End date or ISO string
* @returns Duration in minutes
*/
static getDurationMinutes(start: Date | string, end: Date | string): number;
/**
* Check if two dates are on the same day
* @param date1 - First date
* @param date2 - Second date
* @returns True if same day
*/
static isSameDay(date1: Date, date2: Date): boolean;
/**
* Check if event spans multiple days
* @param start - Start date or ISO string
* @param end - End date or ISO string
* @returns True if spans multiple days
*/
static isMultiDay(start: Date | string, end: Date | string): boolean;
constructor();
}
export declare function createDateCalculator(config: CalendarConfig): DateCalculator;

View file

@ -0,0 +1,260 @@
/**
* DateCalculator - Centralized date calculation logic for calendar
* Handles all date computations with proper week start handling
*/
export class DateCalculator {
/**
* Initialize DateCalculator with configuration
* @param config - Calendar configuration
*/
static initialize(config) {
DateCalculator.config = config;
}
/**
* Validate that a date is valid
* @param date - Date to validate
* @param methodName - Name of calling method for error messages
* @throws Error if date is invalid
*/
static validateDate(date, methodName) {
if (!date || !(date instanceof Date) || isNaN(date.getTime())) {
throw new Error(`${methodName}: Invalid date provided - ${date}`);
}
}
/**
* Get dates for work week using ISO 8601 day numbering (Monday=1, Sunday=7)
* @param weekStart - Any date in the week
* @returns Array of dates for the configured work days
*/
static getWorkWeekDates(weekStart) {
DateCalculator.validateDate(weekStart, 'getWorkWeekDates');
const dates = [];
const workWeekSettings = DateCalculator.config.getWorkWeekSettings();
// Always use ISO week start (Monday)
const mondayOfWeek = DateCalculator.getISOWeekStart(weekStart);
// Calculate dates for each work day using ISO numbering
workWeekSettings.workDays.forEach(isoDay => {
const date = new Date(mondayOfWeek);
// ISO day 1=Monday is +0 days, ISO day 7=Sunday is +6 days
const daysFromMonday = isoDay === 7 ? 6 : isoDay - 1;
date.setDate(mondayOfWeek.getDate() + daysFromMonday);
dates.push(date);
});
return dates;
}
/**
* Get the start of the ISO week (Monday) for a given date
* @param date - Any date in the week
* @returns The Monday of the ISO week
*/
static getISOWeekStart(date) {
DateCalculator.validateDate(date, 'getISOWeekStart');
const monday = new Date(date);
const currentDay = monday.getDay();
const daysToSubtract = currentDay === 0 ? 6 : currentDay - 1;
monday.setDate(monday.getDate() - daysToSubtract);
monday.setHours(0, 0, 0, 0);
return monday;
}
/**
* Get the end of the ISO week for a given date
* @param date - Any date in the week
* @returns The end date of the ISO week (Sunday)
*/
static getWeekEnd(date) {
DateCalculator.validateDate(date, 'getWeekEnd');
const weekStart = DateCalculator.getISOWeekStart(date);
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekStart.getDate() + 6);
weekEnd.setHours(23, 59, 59, 999);
return weekEnd;
}
/**
* Get week number for a date (ISO 8601)
* @param date - The date to get week number for
* @returns Week number (1-53)
*/
static getWeekNumber(date) {
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
const dayNum = d.getUTCDay() || 7;
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
return Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
}
/**
* Format a date range with customizable options
* @param start - Start date
* @param end - End date
* @param options - Formatting options
* @returns Formatted date range string
*/
static formatDateRange(start, end, options = {}) {
const { locale = 'en-US', month = 'short', day = 'numeric' } = options;
const startYear = start.getFullYear();
const endYear = end.getFullYear();
const formatter = new Intl.DateTimeFormat(locale, {
month,
day,
year: startYear !== endYear ? 'numeric' : undefined
});
// @ts-ignore
if (typeof formatter.formatRange === 'function') {
// @ts-ignore
return formatter.formatRange(start, end);
}
return `${formatter.format(start)} - ${formatter.format(end)}`;
}
/**
* Format a date to ISO date string (YYYY-MM-DD)
* @param date - Date to format
* @returns ISO date string
*/
static formatISODate(date) {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
}
/**
* Check if a date is today
* @param date - Date to check
* @returns True if the date is today
*/
static isToday(date) {
const today = new Date();
return date.toDateString() === today.toDateString();
}
/**
* Add days to a date
* @param date - Base date
* @param days - Number of days to add (can be negative)
* @returns New date
*/
static addDays(date, days) {
const result = new Date(date);
result.setDate(result.getDate() + days);
return result;
}
/**
* Add weeks to a date
* @param date - Base date
* @param weeks - Number of weeks to add (can be negative)
* @returns New date
*/
static addWeeks(date, weeks) {
return DateCalculator.addDays(date, weeks * 7);
}
/**
* Get all dates in a week
* @param weekStart - Start of the week
* @returns Array of 7 dates for the full week
*/
static getFullWeekDates(weekStart) {
const dates = [];
for (let i = 0; i < 7; i++) {
dates.push(DateCalculator.addDays(weekStart, i));
}
return dates;
}
/**
* Get the day name for a date using Intl.DateTimeFormat
* @param date - Date to get day name for
* @param format - 'short' or 'long'
* @returns Day name
*/
static getDayName(date, format = 'short') {
const formatter = new Intl.DateTimeFormat('en-US', {
weekday: format
});
return formatter.format(date);
}
/**
* Format time to HH:MM
* @param date - Date to format
* @returns Time string
*/
static formatTime(date) {
return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
}
/**
* Format time to 12-hour format
* @param date - Date to format
* @returns 12-hour time string
*/
static formatTime12(date) {
const hours = date.getHours();
const minutes = date.getMinutes();
const period = hours >= 12 ? 'PM' : 'AM';
const displayHours = hours % 12 || 12;
return `${displayHours}:${String(minutes).padStart(2, '0')} ${period}`;
}
/**
* Convert minutes since midnight to time string
* @param minutes - Minutes since midnight
* @returns Time string
*/
static minutesToTime(minutes) {
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
const period = hours >= 12 ? 'PM' : 'AM';
const displayHours = hours % 12 || 12;
return `${displayHours}:${String(mins).padStart(2, '0')} ${period}`;
}
/**
* Convert time string to minutes since midnight
* @param timeStr - Time string
* @returns Minutes since midnight
*/
static timeToMinutes(timeStr) {
const [time] = timeStr.split('T').pop().split('.');
const [hours, minutes] = time.split(':').map(Number);
return hours * 60 + minutes;
}
/**
* Get minutes since start of day
* @param date - Date or ISO string
* @returns Minutes since midnight
*/
static getMinutesSinceMidnight(date) {
const d = typeof date === 'string' ? new Date(date) : date;
return d.getHours() * 60 + d.getMinutes();
}
/**
* Calculate duration in minutes between two dates
* @param start - Start date or ISO string
* @param end - End date or ISO string
* @returns Duration in minutes
*/
static getDurationMinutes(start, end) {
const startDate = typeof start === 'string' ? new Date(start) : start;
const endDate = typeof end === 'string' ? new Date(end) : end;
return Math.floor((endDate.getTime() - startDate.getTime()) / 60000);
}
/**
* Check if two dates are on the same day
* @param date1 - First date
* @param date2 - Second date
* @returns True if same day
*/
static isSameDay(date1, date2) {
return date1.toDateString() === date2.toDateString();
}
/**
* Check if event spans multiple days
* @param start - Start date or ISO string
* @param end - End date or ISO string
* @returns True if spans multiple days
*/
static isMultiDay(start, end) {
const startDate = typeof start === 'string' ? new Date(start) : start;
const endDate = typeof end === 'string' ? new Date(end) : end;
return !DateCalculator.isSameDay(startDate, endDate);
}
// Legacy constructor for backward compatibility
constructor() {
// Empty constructor - all methods are now static
}
}
// Legacy factory function - deprecated, use static methods instead
export function createDateCalculator(config) {
DateCalculator.initialize(config);
return new DateCalculator();
}
//# sourceMappingURL=DateCalculator.js.map

File diff suppressed because one or more lines are too long

254
wwwroot/js/utils/DateService.d.ts vendored Normal file
View file

@ -0,0 +1,254 @@
/**
* DateService - Unified date/time service using day.js
* Handles all date operations, timezone conversions, and formatting
*/
import { Configuration } from '../configurations/CalendarConfig';
export declare class DateService {
private timezone;
constructor(config: Configuration);
/**
* Convert local date to UTC ISO string
* @param localDate - Date in local timezone
* @returns ISO string in UTC (with 'Z' suffix)
*/
toUTC(localDate: Date): string;
/**
* Convert UTC ISO string to local date
* @param utcString - ISO string in UTC
* @returns Date in local timezone
*/
fromUTC(utcString: string): Date;
/**
* Format time as HH:mm or HH:mm:ss
* @param date - Date to format
* @param showSeconds - Include seconds in output
* @returns Formatted time string
*/
formatTime(date: Date, showSeconds?: boolean): string;
/**
* Format time range as "HH:mm - HH:mm"
* @param start - Start date
* @param end - End date
* @returns Formatted time range
*/
formatTimeRange(start: Date, end: Date): string;
/**
* Format date and time in technical format: yyyy-MM-dd HH:mm:ss
* @param date - Date to format
* @returns Technical datetime string
*/
formatTechnicalDateTime(date: Date): string;
/**
* Format date as yyyy-MM-dd
* @param date - Date to format
* @returns ISO date string
*/
formatDate(date: Date): string;
/**
* Format date as "Month Year" (e.g., "January 2025")
* @param date - Date to format
* @param locale - Locale for month name (default: 'en-US')
* @returns Formatted month and year
*/
formatMonthYear(date: Date, locale?: string): string;
/**
* Format date as ISO string (same as formatDate for compatibility)
* @param date - Date to format
* @returns ISO date string
*/
formatISODate(date: Date): string;
/**
* Format time in 12-hour format with AM/PM
* @param date - Date to format
* @returns Time string in 12-hour format (e.g., "2:30 PM")
*/
formatTime12(date: Date): string;
/**
* Get day name for a date
* @param date - Date to get day name for
* @param format - 'short' (e.g., 'Mon') or 'long' (e.g., 'Monday')
* @param locale - Locale for day name (default: 'da-DK')
* @returns Day name
*/
getDayName(date: Date, format?: 'short' | 'long', locale?: string): string;
/**
* Format a date range with customizable options
* @param start - Start date
* @param end - End date
* @param options - Formatting options
* @returns Formatted date range string
*/
formatDateRange(start: Date, end: Date, options?: {
locale?: string;
month?: 'numeric' | '2-digit' | 'long' | 'short' | 'narrow';
day?: 'numeric' | '2-digit';
year?: 'numeric' | '2-digit';
}): string;
/**
* Convert time string (HH:mm or HH:mm:ss) to total minutes since midnight
* @param timeString - Time in format HH:mm or HH:mm:ss
* @returns Total minutes since midnight
*/
timeToMinutes(timeString: string): number;
/**
* Convert total minutes since midnight to time string HH:mm
* @param totalMinutes - Minutes since midnight
* @returns Time string in format HH:mm
*/
minutesToTime(totalMinutes: number): string;
/**
* Format time from total minutes (alias for minutesToTime)
* @param totalMinutes - Minutes since midnight
* @returns Time string in format HH:mm
*/
formatTimeFromMinutes(totalMinutes: number): string;
/**
* Get minutes since midnight for a given date
* @param date - Date to calculate from
* @returns Minutes since midnight
*/
getMinutesSinceMidnight(date: Date): number;
/**
* Calculate duration in minutes between two dates
* @param start - Start date or ISO string
* @param end - End date or ISO string
* @returns Duration in minutes
*/
getDurationMinutes(start: Date | string, end: Date | string): number;
/**
* Get start and end of week (Monday to Sunday)
* @param date - Reference date
* @returns Object with start and end dates
*/
getWeekBounds(date: Date): {
start: Date;
end: Date;
};
/**
* Add weeks to a date
* @param date - Base date
* @param weeks - Number of weeks to add (can be negative)
* @returns New date
*/
addWeeks(date: Date, weeks: number): Date;
/**
* Add months to a date
* @param date - Base date
* @param months - Number of months to add (can be negative)
* @returns New date
*/
addMonths(date: Date, months: number): Date;
/**
* Get ISO week number (1-53)
* @param date - Date to get week number for
* @returns ISO week number
*/
getWeekNumber(date: Date): number;
/**
* Get all dates in a full week (7 days starting from given date)
* @param weekStart - Start date of the week
* @returns Array of 7 dates
*/
getFullWeekDates(weekStart: Date): Date[];
/**
* Get dates for work week using ISO 8601 day numbering (Monday=1, Sunday=7)
* @param weekStart - Any date in the week
* @param workDays - Array of ISO day numbers (1=Monday, 7=Sunday)
* @returns Array of dates for the specified work days
*/
getWorkWeekDates(weekStart: Date, workDays: number[]): Date[];
/**
* Create a date at a specific time (minutes since midnight)
* @param baseDate - Base date (date component)
* @param totalMinutes - Minutes since midnight
* @returns New date with specified time
*/
createDateAtTime(baseDate: Date, totalMinutes: number): Date;
/**
* Snap date to nearest interval
* @param date - Date to snap
* @param intervalMinutes - Snap interval in minutes
* @returns Snapped date
*/
snapToInterval(date: Date, intervalMinutes: number): Date;
/**
* Check if two dates are the same day
* @param date1 - First date
* @param date2 - Second date
* @returns True if same day
*/
isSameDay(date1: Date, date2: Date): boolean;
/**
* Get start of day
* @param date - Date
* @returns Start of day (00:00:00)
*/
startOfDay(date: Date): Date;
/**
* Get end of day
* @param date - Date
* @returns End of day (23:59:59.999)
*/
endOfDay(date: Date): Date;
/**
* Add days to a date
* @param date - Base date
* @param days - Number of days to add (can be negative)
* @returns New date
*/
addDays(date: Date, days: number): Date;
/**
* Add minutes to a date
* @param date - Base date
* @param minutes - Number of minutes to add (can be negative)
* @returns New date
*/
addMinutes(date: Date, minutes: number): Date;
/**
* Parse ISO string to date
* @param isoString - ISO date string
* @returns Parsed date
*/
parseISO(isoString: string): Date;
/**
* Check if date is valid
* @param date - Date to check
* @returns True if valid
*/
isValid(date: Date): boolean;
/**
* Calculate difference in calendar days between two dates
* @param date1 - First date
* @param date2 - Second date
* @returns Number of calendar days between dates (can be negative)
*/
differenceInCalendarDays(date1: Date, date2: Date): number;
/**
* Validate date range (start must be before or equal to end)
* @param start - Start date
* @param end - End date
* @returns True if valid range
*/
isValidRange(start: Date, end: Date): boolean;
/**
* Check if date is within reasonable bounds (1900-2100)
* @param date - Date to check
* @returns True if within bounds
*/
isWithinBounds(date: Date): boolean;
/**
* Validate date with comprehensive checks
* @param date - Date to validate
* @param options - Validation options
* @returns Validation result with error message
*/
validateDate(date: Date, options?: {
requireFuture?: boolean;
requirePast?: boolean;
minDate?: Date;
maxDate?: Date;
}): {
valid: boolean;
error?: string;
};
}

View file

@ -0,0 +1,418 @@
/**
* DateService - Unified date/time service using day.js
* Handles all date operations, timezone conversions, and formatting
*/
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
import isoWeek from 'dayjs/plugin/isoWeek';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
// Enable day.js plugins
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(isoWeek);
dayjs.extend(customParseFormat);
dayjs.extend(isSameOrAfter);
dayjs.extend(isSameOrBefore);
export class DateService {
constructor(config) {
this.timezone = config.timeFormatConfig.timezone;
}
// ============================================
// CORE CONVERSIONS
// ============================================
/**
* Convert local date to UTC ISO string
* @param localDate - Date in local timezone
* @returns ISO string in UTC (with 'Z' suffix)
*/
toUTC(localDate) {
return dayjs.tz(localDate, this.timezone).utc().toISOString();
}
/**
* Convert UTC ISO string to local date
* @param utcString - ISO string in UTC
* @returns Date in local timezone
*/
fromUTC(utcString) {
return dayjs.utc(utcString).tz(this.timezone).toDate();
}
// ============================================
// FORMATTING
// ============================================
/**
* Format time as HH:mm or HH:mm:ss
* @param date - Date to format
* @param showSeconds - Include seconds in output
* @returns Formatted time string
*/
formatTime(date, showSeconds = false) {
const pattern = showSeconds ? 'HH:mm:ss' : 'HH:mm';
return dayjs(date).format(pattern);
}
/**
* Format time range as "HH:mm - HH:mm"
* @param start - Start date
* @param end - End date
* @returns Formatted time range
*/
formatTimeRange(start, end) {
return `${this.formatTime(start)} - ${this.formatTime(end)}`;
}
/**
* Format date and time in technical format: yyyy-MM-dd HH:mm:ss
* @param date - Date to format
* @returns Technical datetime string
*/
formatTechnicalDateTime(date) {
return dayjs(date).format('YYYY-MM-DD HH:mm:ss');
}
/**
* Format date as yyyy-MM-dd
* @param date - Date to format
* @returns ISO date string
*/
formatDate(date) {
return dayjs(date).format('YYYY-MM-DD');
}
/**
* Format date as "Month Year" (e.g., "January 2025")
* @param date - Date to format
* @param locale - Locale for month name (default: 'en-US')
* @returns Formatted month and year
*/
formatMonthYear(date, locale = 'en-US') {
return date.toLocaleDateString(locale, { month: 'long', year: 'numeric' });
}
/**
* Format date as ISO string (same as formatDate for compatibility)
* @param date - Date to format
* @returns ISO date string
*/
formatISODate(date) {
return this.formatDate(date);
}
/**
* Format time in 12-hour format with AM/PM
* @param date - Date to format
* @returns Time string in 12-hour format (e.g., "2:30 PM")
*/
formatTime12(date) {
return dayjs(date).format('h:mm A');
}
/**
* Get day name for a date
* @param date - Date to get day name for
* @param format - 'short' (e.g., 'Mon') or 'long' (e.g., 'Monday')
* @param locale - Locale for day name (default: 'da-DK')
* @returns Day name
*/
getDayName(date, format = 'short', locale = 'da-DK') {
const formatter = new Intl.DateTimeFormat(locale, {
weekday: format
});
return formatter.format(date);
}
/**
* Format a date range with customizable options
* @param start - Start date
* @param end - End date
* @param options - Formatting options
* @returns Formatted date range string
*/
formatDateRange(start, end, options = {}) {
const { locale = 'en-US', month = 'short', day = 'numeric' } = options;
const startYear = start.getFullYear();
const endYear = end.getFullYear();
const formatter = new Intl.DateTimeFormat(locale, {
month,
day,
year: startYear !== endYear ? 'numeric' : undefined
});
// @ts-ignore - formatRange is available in modern browsers
if (typeof formatter.formatRange === 'function') {
// @ts-ignore
return formatter.formatRange(start, end);
}
return `${formatter.format(start)} - ${formatter.format(end)}`;
}
// ============================================
// TIME CALCULATIONS
// ============================================
/**
* Convert time string (HH:mm or HH:mm:ss) to total minutes since midnight
* @param timeString - Time in format HH:mm or HH:mm:ss
* @returns Total minutes since midnight
*/
timeToMinutes(timeString) {
const parts = timeString.split(':').map(Number);
const hours = parts[0] || 0;
const minutes = parts[1] || 0;
return hours * 60 + minutes;
}
/**
* Convert total minutes since midnight to time string HH:mm
* @param totalMinutes - Minutes since midnight
* @returns Time string in format HH:mm
*/
minutesToTime(totalMinutes) {
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
return dayjs().hour(hours).minute(minutes).format('HH:mm');
}
/**
* Format time from total minutes (alias for minutesToTime)
* @param totalMinutes - Minutes since midnight
* @returns Time string in format HH:mm
*/
formatTimeFromMinutes(totalMinutes) {
return this.minutesToTime(totalMinutes);
}
/**
* Get minutes since midnight for a given date
* @param date - Date to calculate from
* @returns Minutes since midnight
*/
getMinutesSinceMidnight(date) {
const d = dayjs(date);
return d.hour() * 60 + d.minute();
}
/**
* Calculate duration in minutes between two dates
* @param start - Start date or ISO string
* @param end - End date or ISO string
* @returns Duration in minutes
*/
getDurationMinutes(start, end) {
const startDate = dayjs(start);
const endDate = dayjs(end);
return endDate.diff(startDate, 'minute');
}
// ============================================
// WEEK OPERATIONS
// ============================================
/**
* Get start and end of week (Monday to Sunday)
* @param date - Reference date
* @returns Object with start and end dates
*/
getWeekBounds(date) {
const d = dayjs(date);
return {
start: d.startOf('week').add(1, 'day').toDate(), // Monday (day.js week starts on Sunday)
end: d.endOf('week').add(1, 'day').toDate() // Sunday
};
}
/**
* Add weeks to a date
* @param date - Base date
* @param weeks - Number of weeks to add (can be negative)
* @returns New date
*/
addWeeks(date, weeks) {
return dayjs(date).add(weeks, 'week').toDate();
}
/**
* Add months to a date
* @param date - Base date
* @param months - Number of months to add (can be negative)
* @returns New date
*/
addMonths(date, months) {
return dayjs(date).add(months, 'month').toDate();
}
/**
* Get ISO week number (1-53)
* @param date - Date to get week number for
* @returns ISO week number
*/
getWeekNumber(date) {
return dayjs(date).isoWeek();
}
/**
* Get all dates in a full week (7 days starting from given date)
* @param weekStart - Start date of the week
* @returns Array of 7 dates
*/
getFullWeekDates(weekStart) {
const dates = [];
for (let i = 0; i < 7; i++) {
dates.push(this.addDays(weekStart, i));
}
return dates;
}
/**
* Get dates for work week using ISO 8601 day numbering (Monday=1, Sunday=7)
* @param weekStart - Any date in the week
* @param workDays - Array of ISO day numbers (1=Monday, 7=Sunday)
* @returns Array of dates for the specified work days
*/
getWorkWeekDates(weekStart, workDays) {
const dates = [];
// Get Monday of the week
const weekBounds = this.getWeekBounds(weekStart);
const mondayOfWeek = this.startOfDay(weekBounds.start);
// Calculate dates for each work day using ISO numbering
workDays.forEach(isoDay => {
const date = new Date(mondayOfWeek);
// ISO day 1=Monday is +0 days, ISO day 7=Sunday is +6 days
const daysFromMonday = isoDay === 7 ? 6 : isoDay - 1;
date.setDate(mondayOfWeek.getDate() + daysFromMonday);
dates.push(date);
});
return dates;
}
// ============================================
// GRID HELPERS
// ============================================
/**
* Create a date at a specific time (minutes since midnight)
* @param baseDate - Base date (date component)
* @param totalMinutes - Minutes since midnight
* @returns New date with specified time
*/
createDateAtTime(baseDate, totalMinutes) {
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
return dayjs(baseDate).startOf('day').hour(hours).minute(minutes).toDate();
}
/**
* Snap date to nearest interval
* @param date - Date to snap
* @param intervalMinutes - Snap interval in minutes
* @returns Snapped date
*/
snapToInterval(date, intervalMinutes) {
const minutes = this.getMinutesSinceMidnight(date);
const snappedMinutes = Math.round(minutes / intervalMinutes) * intervalMinutes;
return this.createDateAtTime(date, snappedMinutes);
}
// ============================================
// UTILITY METHODS
// ============================================
/**
* Check if two dates are the same day
* @param date1 - First date
* @param date2 - Second date
* @returns True if same day
*/
isSameDay(date1, date2) {
return dayjs(date1).isSame(date2, 'day');
}
/**
* Get start of day
* @param date - Date
* @returns Start of day (00:00:00)
*/
startOfDay(date) {
return dayjs(date).startOf('day').toDate();
}
/**
* Get end of day
* @param date - Date
* @returns End of day (23:59:59.999)
*/
endOfDay(date) {
return dayjs(date).endOf('day').toDate();
}
/**
* Add days to a date
* @param date - Base date
* @param days - Number of days to add (can be negative)
* @returns New date
*/
addDays(date, days) {
return dayjs(date).add(days, 'day').toDate();
}
/**
* Add minutes to a date
* @param date - Base date
* @param minutes - Number of minutes to add (can be negative)
* @returns New date
*/
addMinutes(date, minutes) {
return dayjs(date).add(minutes, 'minute').toDate();
}
/**
* Parse ISO string to date
* @param isoString - ISO date string
* @returns Parsed date
*/
parseISO(isoString) {
return dayjs(isoString).toDate();
}
/**
* Check if date is valid
* @param date - Date to check
* @returns True if valid
*/
isValid(date) {
return dayjs(date).isValid();
}
/**
* Calculate difference in calendar days between two dates
* @param date1 - First date
* @param date2 - Second date
* @returns Number of calendar days between dates (can be negative)
*/
differenceInCalendarDays(date1, date2) {
const d1 = dayjs(date1).startOf('day');
const d2 = dayjs(date2).startOf('day');
return d1.diff(d2, 'day');
}
/**
* Validate date range (start must be before or equal to end)
* @param start - Start date
* @param end - End date
* @returns True if valid range
*/
isValidRange(start, end) {
if (!this.isValid(start) || !this.isValid(end)) {
return false;
}
return start.getTime() <= end.getTime();
}
/**
* Check if date is within reasonable bounds (1900-2100)
* @param date - Date to check
* @returns True if within bounds
*/
isWithinBounds(date) {
if (!this.isValid(date)) {
return false;
}
const year = date.getFullYear();
return year >= 1900 && year <= 2100;
}
/**
* Validate date with comprehensive checks
* @param date - Date to validate
* @param options - Validation options
* @returns Validation result with error message
*/
validateDate(date, options = {}) {
if (!this.isValid(date)) {
return { valid: false, error: 'Invalid date' };
}
if (!this.isWithinBounds(date)) {
return { valid: false, error: 'Date out of bounds (1900-2100)' };
}
const now = new Date();
if (options.requireFuture && date <= now) {
return { valid: false, error: 'Date must be in the future' };
}
if (options.requirePast && date >= now) {
return { valid: false, error: 'Date must be in the past' };
}
if (options.minDate && date < options.minDate) {
return { valid: false, error: `Date must be after ${this.formatDate(options.minDate)}` };
}
if (options.maxDate && date > options.maxDate) {
return { valid: false, error: `Date must be before ${this.formatDate(options.maxDate)}` };
}
return { valid: true };
}
}
//# sourceMappingURL=DateService.js.map

File diff suppressed because one or more lines are too long

33
wwwroot/js/utils/OverlapDetector.d.ts vendored Normal file
View file

@ -0,0 +1,33 @@
/**
* OverlapDetector - Ren tidbaseret overlap detection
* Ingen DOM manipulation, kun tidsberegninger
*/
import { CalendarEvent } from '../types/CalendarTypes';
export type EventId = string & {
readonly __brand: 'EventId';
};
export type OverlapResult = {
overlappingEvents: CalendarEvent[];
stackLinks: Map<EventId, StackLink>;
};
export interface StackLink {
prev?: EventId;
next?: EventId;
stackLevel: number;
}
export declare class OverlapDetector {
/**
* Resolver hvilke events et givent event overlapper med i en kolonne
* @param event - CalendarEvent der skal checkes for overlap
* @param columnEvents - Array af CalendarEvent objekter i kolonnen
* @returns Array af events som det givne event overlapper med
*/
resolveOverlap(event: CalendarEvent, columnEvents: CalendarEvent[]): CalendarEvent[];
/**
* Dekorerer events med stack linking data
* @param newEvent - Det nye event der skal tilføjes
* @param overlappingEvents - Events som det nye event overlapper med
* @returns OverlapResult med overlappende events og stack links
*/
decorateWithStackLinks(newEvent: CalendarEvent, overlappingEvents: CalendarEvent[]): OverlapResult;
}

View file

@ -0,0 +1,52 @@
/**
* OverlapDetector - Ren tidbaseret overlap detection
* Ingen DOM manipulation, kun tidsberegninger
*/
export class OverlapDetector {
/**
* Resolver hvilke events et givent event overlapper med i en kolonne
* @param event - CalendarEvent der skal checkes for overlap
* @param columnEvents - Array af CalendarEvent objekter i kolonnen
* @returns Array af events som det givne event overlapper med
*/
resolveOverlap(event, columnEvents) {
return columnEvents.filter(existingEvent => {
// To events overlapper hvis:
// event starter før existing slutter OG
// event slutter efter existing starter
return event.start < existingEvent.end && event.end > existingEvent.start;
});
}
/**
* Dekorerer events med stack linking data
* @param newEvent - Det nye event der skal tilføjes
* @param overlappingEvents - Events som det nye event overlapper med
* @returns OverlapResult med overlappende events og stack links
*/
decorateWithStackLinks(newEvent, overlappingEvents) {
const stackLinks = new Map();
if (overlappingEvents.length === 0) {
return {
overlappingEvents: [],
stackLinks
};
}
// Kombiner nyt event med eksisterende og sortér efter start tid (tidligste første)
const allEvents = [...overlappingEvents, newEvent].sort((a, b) => a.start.getTime() - b.start.getTime());
// Opret sammenhængende kæde - alle events bindes sammen
allEvents.forEach((event, index) => {
const stackLink = {
stackLevel: index,
prev: index > 0 ? allEvents[index - 1].id : undefined,
next: index < allEvents.length - 1 ? allEvents[index + 1].id : undefined
};
stackLinks.set(event.id, stackLink);
});
overlappingEvents.push(newEvent);
return {
overlappingEvents,
stackLinks
};
}
}
//# sourceMappingURL=OverlapDetector.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"OverlapDetector.js","sourceRoot":"","sources":["../../../src/utils/OverlapDetector.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAkBH,MAAM,OAAO,eAAe;IAE1B;;;;;OAKG;IACI,cAAc,CAAC,KAAoB,EAAE,YAA6B;QACvE,OAAO,YAAY,CAAC,MAAM,CAAC,aAAa,CAAC,EAAE;YACzC,6BAA6B;YAC7B,wCAAwC;YACxC,uCAAuC;YACvC,OAAO,KAAK,CAAC,KAAK,GAAG,aAAa,CAAC,GAAG,IAAI,KAAK,CAAC,GAAG,GAAG,aAAa,CAAC,KAAK,CAAC;QAC5E,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;;;;OAKG;IACI,sBAAsB,CAAC,QAAuB,EAAE,iBAAkC;QACvF,MAAM,UAAU,GAAG,IAAI,GAAG,EAAsB,CAAC;QAEjD,IAAI,iBAAiB,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACnC,OAAO;gBACL,iBAAiB,EAAE,EAAE;gBACrB,UAAU;aACX,CAAC;QACJ,CAAC;QAED,mFAAmF;QACnF,MAAM,SAAS,GAAG,CAAC,GAAG,iBAAiB,EAAE,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAC/D,CAAC,CAAC,KAAK,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,EAAE,CACtC,CAAC;QAEF,wDAAwD;QACxD,SAAS,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;YACjC,MAAM,SAAS,GAAc;gBAC3B,UAAU,EAAE,KAAK;gBACjB,IAAI,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,EAAa,CAAC,CAAC,CAAC,SAAS;gBAChE,IAAI,EAAE,KAAK,GAAG,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,EAAa,CAAC,CAAC,CAAC,SAAS;aACpF,CAAC;YACF,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,EAAa,EAAE,SAAS,CAAC,CAAC;QACjD,CAAC,CAAC,CAAC;QACH,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACjC,OAAO;YACL,iBAAiB;YACjB,UAAU;SACX,CAAC;IACJ,CAAC;CACF"}

101
wwwroot/js/utils/PositionUtils.d.ts vendored Normal file
View file

@ -0,0 +1,101 @@
import { Configuration } from '../configurations/CalendarConfig';
import { IColumnBounds } from './ColumnDetectionUtils';
import { DateService } from './DateService';
/**
* 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 declare class PositionUtils {
private dateService;
private config;
constructor(dateService: DateService, config: Configuration);
/**
* Convert minutes to pixels
*/
minutesToPixels(minutes: number): number;
/**
* Convert pixels to minutes
*/
pixelsToMinutes(pixels: number): number;
/**
* Convert time (HH:MM) to pixels from day start using DateService
*/
timeToPixels(timeString: string): number;
/**
* Convert Date object to pixels from day start using DateService
*/
dateToPixels(date: Date): number;
/**
* Convert pixels to time using DateService
*/
pixelsToTime(pixels: number): string;
/**
* Beregn event position og størrelse
*/
calculateEventPosition(startTime: string | Date, endTime: string | Date): {
top: number;
height: number;
duration: number;
};
/**
* Snap position til grid interval
*/
snapToGrid(pixels: number): number;
/**
* Snap time to interval using DateService
*/
snapTimeToInterval(timeString: string): string;
/**
* Beregn kolonne position for overlappende events
*/
calculateColumnPosition(eventIndex: number, totalColumns: number, containerWidth: number): {
left: number;
width: number;
};
/**
* Check om to events overlapper i tid
*/
eventsOverlap(start1: string | Date, end1: string | Date, start2: string | Date, end2: string | Date): boolean;
/**
* Beregn Y position fra mouse/touch koordinat
*/
getPositionFromCoordinate(clientY: number, column: IColumnBounds): number;
/**
* Valider at tid er inden for arbejdstimer
*/
isWithinWorkHours(timeString: string): boolean;
/**
* Valider at tid er inden for dag grænser
*/
isWithinDayBounds(timeString: string): boolean;
/**
* Hent minimum event højde i pixels
*/
getMinimumEventHeight(): number;
/**
* Hent maksimum event højde i pixels (hele dagen)
*/
getMaximumEventHeight(): number;
/**
* Beregn total kalender højde
*/
getTotalCalendarHeight(): number;
/**
* Convert ISO datetime to time string with UTC-to-local conversion
*/
isoToTimeString(isoString: string): string;
/**
* Convert time string to ISO datetime using DateService with timezone handling
*/
timeStringToIso(timeString: string, date?: Date): string;
/**
* Calculate event duration using DateService
*/
calculateDuration(startTime: string | Date, endTime: string | Date): number;
/**
* Format duration to readable text (Danish)
*/
formatDuration(minutes: number): string;
}

View file

@ -0,0 +1,209 @@
import { TimeFormatter } from './TimeFormatter';
/**
* 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 {
constructor(dateService, config) {
this.dateService = dateService;
this.config = config;
}
/**
* Convert minutes to pixels
*/
minutesToPixels(minutes) {
const gridSettings = this.config.gridSettings;
const pixelsPerHour = gridSettings.hourHeight;
return (minutes / 60) * pixelsPerHour;
}
/**
* Convert pixels to minutes
*/
pixelsToMinutes(pixels) {
const gridSettings = this.config.gridSettings;
const pixelsPerHour = gridSettings.hourHeight;
return (pixels / pixelsPerHour) * 60;
}
/**
* Convert time (HH:MM) to pixels from day start using DateService
*/
timeToPixels(timeString) {
const totalMinutes = this.dateService.timeToMinutes(timeString);
const gridSettings = this.config.gridSettings;
const dayStartMinutes = gridSettings.dayStartHour * 60;
const minutesFromDayStart = totalMinutes - dayStartMinutes;
return this.minutesToPixels(minutesFromDayStart);
}
/**
* Convert Date object to pixels from day start using DateService
*/
dateToPixels(date) {
const totalMinutes = this.dateService.getMinutesSinceMidnight(date);
const gridSettings = this.config.gridSettings;
const dayStartMinutes = gridSettings.dayStartHour * 60;
const minutesFromDayStart = totalMinutes - dayStartMinutes;
return this.minutesToPixels(minutesFromDayStart);
}
/**
* Convert pixels to time using DateService
*/
pixelsToTime(pixels) {
const minutes = this.pixelsToMinutes(pixels);
const gridSettings = this.config.gridSettings;
const dayStartMinutes = gridSettings.dayStartHour * 60;
const totalMinutes = dayStartMinutes + minutes;
return this.dateService.minutesToTime(totalMinutes);
}
/**
* Beregn event position og størrelse
*/
calculateEventPosition(startTime, endTime) {
let startPixels;
let endPixels;
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
*/
snapToGrid(pixels) {
const gridSettings = this.config.gridSettings;
const snapInterval = gridSettings.snapInterval;
const snapPixels = this.minutesToPixels(snapInterval);
return Math.round(pixels / snapPixels) * snapPixels;
}
/**
* Snap time to interval using DateService
*/
snapTimeToInterval(timeString) {
const totalMinutes = this.dateService.timeToMinutes(timeString);
const gridSettings = this.config.gridSettings;
const snapInterval = gridSettings.snapInterval;
const snappedMinutes = Math.round(totalMinutes / snapInterval) * snapInterval;
return this.dateService.minutesToTime(snappedMinutes);
}
/**
* Beregn kolonne position for overlappende events
*/
calculateColumnPosition(eventIndex, totalColumns, containerWidth) {
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
*/
eventsOverlap(start1, end1, 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
*/
getPositionFromCoordinate(clientY, column) {
const relativeY = clientY - column.boundingClientRect.top;
// Snap til grid
return this.snapToGrid(relativeY);
}
/**
* Valider at tid er inden for arbejdstimer
*/
isWithinWorkHours(timeString) {
const [hours] = timeString.split(':').map(Number);
const gridSettings = this.config.gridSettings;
return hours >= gridSettings.workStartHour && hours < gridSettings.workEndHour;
}
/**
* Valider at tid er inden for dag grænser
*/
isWithinDayBounds(timeString) {
const [hours] = timeString.split(':').map(Number);
const gridSettings = this.config.gridSettings;
return hours >= gridSettings.dayStartHour && hours < gridSettings.dayEndHour;
}
/**
* Hent minimum event højde i pixels
*/
getMinimumEventHeight() {
// Minimum 15 minutter
return this.minutesToPixels(15);
}
/**
* Hent maksimum event højde i pixels (hele dagen)
*/
getMaximumEventHeight() {
const gridSettings = this.config.gridSettings;
const dayDurationHours = gridSettings.dayEndHour - gridSettings.dayStartHour;
return dayDurationHours * gridSettings.hourHeight;
}
/**
* Beregn total kalender højde
*/
getTotalCalendarHeight() {
return this.getMaximumEventHeight();
}
/**
* Convert ISO datetime to time string with UTC-to-local conversion
*/
isoToTimeString(isoString) {
const date = new Date(isoString);
return TimeFormatter.formatTime(date);
}
/**
* Convert time string to ISO datetime using DateService with timezone handling
*/
timeStringToIso(timeString, date = new Date()) {
const totalMinutes = this.dateService.timeToMinutes(timeString);
const newDate = this.dateService.createDateAtTime(date, totalMinutes);
return this.dateService.toUTC(newDate);
}
/**
* Calculate event duration using DateService
*/
calculateDuration(startTime, endTime) {
return this.dateService.getDurationMinutes(startTime, endTime);
}
/**
* Format duration to readable text (Danish)
*/
formatDuration(minutes) {
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`;
}
}
//# sourceMappingURL=PositionUtils.js.map

File diff suppressed because one or more lines are too long

45
wwwroot/js/utils/TimeFormatter.d.ts vendored Normal file
View file

@ -0,0 +1,45 @@
/**
* TimeFormatter - Centralized time formatting with timezone support
* Now uses DateService internally for all date/time operations
*
* Handles conversion from UTC/Zulu time to configured timezone (default: Europe/Copenhagen)
* Supports both 12-hour and 24-hour format configuration
*
* All events in the system are stored in UTC and must be converted to local timezone
*/
import { ITimeFormatConfig } from '../configurations/TimeFormatConfig';
export declare class TimeFormatter {
private static settings;
private static dateService;
private static getDateService;
/**
* Configure time formatting settings
* Must be called before using TimeFormatter
*/
static configure(settings: ITimeFormatConfig): void;
/**
* Convert UTC date to configured timezone (internal helper)
* @param utcDate - Date in UTC (or ISO string)
* @returns Date object adjusted to configured timezone
*/
private static convertToLocalTime;
/**
* Format time in 24-hour format using DateService (internal helper)
* @param date - Date to format
* @returns Formatted time string (e.g., "09:00")
*/
private static format24Hour;
/**
* Format time according to current configuration
* @param date - Date to format
* @returns Formatted time string
*/
static formatTime(date: Date): string;
/**
* Format time range (start - end) using DateService
* @param startDate - Start date
* @param endDate - End date
* @returns Formatted time range string (e.g., "09:00 - 10:30")
*/
static formatTimeRange(startDate: Date, endDate: Date): string;
}

View file

@ -0,0 +1,92 @@
/**
* TimeFormatter - Centralized time formatting with timezone support
* Now uses DateService internally for all date/time operations
*
* Handles conversion from UTC/Zulu time to configured timezone (default: Europe/Copenhagen)
* Supports both 12-hour and 24-hour format configuration
*
* All events in the system are stored in UTC and must be converted to local timezone
*/
import { DateService } from './DateService';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
// Enable day.js plugins for timezone formatting
dayjs.extend(utc);
dayjs.extend(timezone);
export class TimeFormatter {
static getDateService() {
if (!TimeFormatter.dateService) {
if (!TimeFormatter.settings) {
throw new Error('TimeFormatter must be configured before use. Call TimeFormatter.configure() first.');
}
// Create a minimal config object for DateService
const config = {
timeFormatConfig: {
timezone: TimeFormatter.settings.timezone
}
};
TimeFormatter.dateService = new DateService(config);
}
return TimeFormatter.dateService;
}
/**
* Configure time formatting settings
* Must be called before using TimeFormatter
*/
static configure(settings) {
TimeFormatter.settings = settings;
// Reset DateService to pick up new timezone
TimeFormatter.dateService = null;
}
/**
* 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) {
if (typeof utcDate === 'string') {
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.getDateService().fromUTC(utcString);
}
/**
* 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) {
if (!TimeFormatter.settings) {
throw new Error('TimeFormatter must be configured before use. Call TimeFormatter.configure() first.');
}
// Use day.js directly to format with timezone awareness
const pattern = TimeFormatter.settings.showSeconds ? 'HH:mm:ss' : 'HH:mm';
return dayjs.utc(date).tz(TimeFormatter.settings.timezone).format(pattern);
}
/**
* Format time according to current configuration
* @param date - Date to format
* @returns Formatted time string
*/
static formatTime(date) {
// Always use 24-hour format (12-hour support removed as unused)
return TimeFormatter.format24Hour(date);
}
/**
* Format time range (start - end) using DateService
* @param startDate - Start date
* @param endDate - End date
* @returns Formatted time range string (e.g., "09:00 - 10:30")
*/
static formatTimeRange(startDate, endDate) {
const localStart = TimeFormatter.convertToLocalTime(startDate);
const localEnd = TimeFormatter.convertToLocalTime(endDate);
return TimeFormatter.getDateService().formatTimeRange(localStart, localEnd);
}
}
TimeFormatter.settings = null;
// DateService will be initialized lazily to avoid circular dependency with CalendarConfig
TimeFormatter.dateService = null;
//# sourceMappingURL=TimeFormatter.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"TimeFormatter.js","sourceRoot":"","sources":["../../../src/utils/TimeFormatter.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE5C,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,GAAG,MAAM,kBAAkB,CAAC;AACnC,OAAO,QAAQ,MAAM,uBAAuB,CAAC;AAE7C,gDAAgD;AAChD,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;AAClB,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;AAEvB,MAAM,OAAO,aAAa;IAMhB,MAAM,CAAC,cAAc;QAC3B,IAAI,CAAC,aAAa,CAAC,WAAW,EAAE,CAAC;YAC/B,IAAI,CAAC,aAAa,CAAC,QAAQ,EAAE,CAAC;gBAC5B,MAAM,IAAI,KAAK,CAAC,oFAAoF,CAAC,CAAC;YACxG,CAAC;YACD,iDAAiD;YACjD,MAAM,MAAM,GAAG;gBACb,gBAAgB,EAAE;oBAChB,QAAQ,EAAE,aAAa,CAAC,QAAQ,CAAC,QAAQ;iBAC1C;aACF,CAAC;YACF,aAAa,CAAC,WAAW,GAAG,IAAI,WAAW,CAAC,MAAa,CAAC,CAAC;QAC7D,CAAC;QACD,OAAO,aAAa,CAAC,WAAW,CAAC;IACnC,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,SAAS,CAAC,QAA2B;QAC1C,aAAa,CAAC,QAAQ,GAAG,QAAQ,CAAC;QAClC,4CAA4C;QAC5C,aAAa,CAAC,WAAW,GAAG,IAAI,CAAC;IACnC,CAAC;IAED;;;;OAIG;IACK,MAAM,CAAC,kBAAkB,CAAC,OAAsB;QACtD,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;YAChC,OAAO,aAAa,CAAC,cAAc,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACzD,CAAC;QAED,iFAAiF;QACjF,MAAM,SAAS,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;QACxC,OAAO,aAAa,CAAC,cAAc,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAC3D,CAAC;IAED;;;;OAIG;IACK,MAAM,CAAC,YAAY,CAAC,IAAU;QACpC,IAAI,CAAC,aAAa,CAAC,QAAQ,EAAE,CAAC;YAC5B,MAAM,IAAI,KAAK,CAAC,oFAAoF,CAAC,CAAC;QACxG,CAAC;QAED,wDAAwD;QACxD,MAAM,OAAO,GAAG,aAAa,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC;QAC1E,OAAO,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,aAAa,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC7E,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,UAAU,CAAC,IAAU;QAC1B,gEAAgE;QAChE,OAAO,aAAa,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;IAC1C,CAAC;IAED;;;;;OAKG;IACH,MAAM,CAAC,eAAe,CAAC,SAAe,EAAE,OAAa;QACnD,MAAM,UAAU,GAAG,aAAa,CAAC,kBAAkB,CAAC,SAAS,CAAC,CAAC;QAC/D,MAAM,QAAQ,GAAG,aAAa,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC;QAC3D,OAAO,aAAa,CAAC,cAAc,EAAE,CAAC,eAAe,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;IAC9E,CAAC;;AAjFc,sBAAQ,GAA6B,IAAI,CAAC;AAEzD,0FAA0F;AAC3E,yBAAW,GAAuB,IAAI,CAAC"}

29
wwwroot/js/utils/URLManager.d.ts vendored Normal file
View file

@ -0,0 +1,29 @@
import { IEventBus } from '../types/CalendarTypes';
/**
* URLManager handles URL query parameter parsing and deep linking functionality
* Follows event-driven architecture with no global state
*/
export declare class URLManager {
private eventBus;
constructor(eventBus: IEventBus);
/**
* Parse eventId from URL query parameters
* @returns eventId string or null if not found
*/
parseEventIdFromURL(): string | null;
/**
* Get all query parameters as an object
* @returns object with all query parameters
*/
getAllQueryParams(): Record<string, string>;
/**
* Update URL without page reload (for future use)
* @param params object with parameters to update
*/
updateURL(params: Record<string, string | null>): void;
/**
* Check if current URL has any query parameters
* @returns true if URL has query parameters
*/
hasQueryParams(): boolean;
}

View file

@ -0,0 +1,76 @@
/**
* URLManager handles URL query parameter parsing and deep linking functionality
* Follows event-driven architecture with no global state
*/
export class URLManager {
constructor(eventBus) {
this.eventBus = eventBus;
}
/**
* Parse eventId from URL query parameters
* @returns eventId string or null if not found
*/
parseEventIdFromURL() {
try {
const urlParams = new URLSearchParams(window.location.search);
const eventId = urlParams.get('eventId');
if (eventId && eventId.trim() !== '') {
return eventId.trim();
}
return null;
}
catch (error) {
console.warn('URLManager: Failed to parse URL parameters:', error);
return null;
}
}
/**
* Get all query parameters as an object
* @returns object with all query parameters
*/
getAllQueryParams() {
try {
const urlParams = new URLSearchParams(window.location.search);
const params = {};
for (const [key, value] of urlParams.entries()) {
params[key] = value;
}
return params;
}
catch (error) {
console.warn('URLManager: Failed to parse URL parameters:', error);
return {};
}
}
/**
* Update URL without page reload (for future use)
* @param params object with parameters to update
*/
updateURL(params) {
try {
const url = new URL(window.location.href);
// Update or remove parameters
Object.entries(params).forEach(([key, value]) => {
if (value === null) {
url.searchParams.delete(key);
}
else {
url.searchParams.set(key, value);
}
});
// Update URL without page reload
window.history.replaceState({}, '', url.toString());
}
catch (error) {
console.warn('URLManager: Failed to update URL:', error);
}
}
/**
* Check if current URL has any query parameters
* @returns true if URL has query parameters
*/
hasQueryParams() {
return window.location.search.length > 0;
}
}
//# sourceMappingURL=URLManager.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"URLManager.js","sourceRoot":"","sources":["../../../src/utils/URLManager.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,MAAM,OAAO,UAAU;IAGnB,YAAY,QAAmB;QAC3B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC7B,CAAC;IAED;;;OAGG;IACI,mBAAmB;QACtB,IAAI,CAAC;YACD,MAAM,SAAS,GAAG,IAAI,eAAe,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;YAC9D,MAAM,OAAO,GAAG,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YAEzC,IAAI,OAAO,IAAI,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;gBACnC,OAAO,OAAO,CAAC,IAAI,EAAE,CAAC;YAC1B,CAAC;YAED,OAAO,IAAI,CAAC;QAChB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACb,OAAO,CAAC,IAAI,CAAC,6CAA6C,EAAE,KAAK,CAAC,CAAC;YACnE,OAAO,IAAI,CAAC;QAChB,CAAC;IACL,CAAC;IAED;;;OAGG;IACI,iBAAiB;QACpB,IAAI,CAAC;YACD,MAAM,SAAS,GAAG,IAAI,eAAe,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;YAC9D,MAAM,MAAM,GAA2B,EAAE,CAAC;YAE1C,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,SAAS,CAAC,OAAO,EAAE,EAAE,CAAC;gBAC7C,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;YACxB,CAAC;YAED,OAAO,MAAM,CAAC;QAClB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACb,OAAO,CAAC,IAAI,CAAC,6CAA6C,EAAE,KAAK,CAAC,CAAC;YACnE,OAAO,EAAE,CAAC;QACd,CAAC;IACL,CAAC;IAED;;;OAGG;IACI,SAAS,CAAC,MAAqC;QAClD,IAAI,CAAC;YACD,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;YAE1C,8BAA8B;YAC9B,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE;gBAC5C,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;oBACjB,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBACjC,CAAC;qBAAM,CAAC;oBACJ,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;gBACrC,CAAC;YACL,CAAC,CAAC,CAAC;YAEH,iCAAiC;YACjC,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC;QACxD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACb,OAAO,CAAC,IAAI,CAAC,mCAAmC,EAAE,KAAK,CAAC,CAAC;QAC7D,CAAC;IACL,CAAC;IAED;;;OAGG;IACI,cAAc;QACjB,OAAO,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;IAC7C,CAAC;CACJ"}