Adds I-prefix to all interfaces
This commit is contained in:
parent
80aaab46f2
commit
8ec5f52872
44 changed files with 1731 additions and 1949 deletions
|
|
@ -1,142 +1,142 @@
|
|||
import { CalendarEvent } from '../types/CalendarTypes';
|
||||
|
||||
export interface EventLayout {
|
||||
calenderEvent: CalendarEvent;
|
||||
gridArea: string; // "row-start / col-start / row-end / col-end"
|
||||
startColumn: number;
|
||||
endColumn: number;
|
||||
row: number;
|
||||
columnSpan: number;
|
||||
}
|
||||
|
||||
export class AllDayLayoutEngine {
|
||||
private weekDates: string[];
|
||||
private tracks: boolean[][];
|
||||
|
||||
constructor(weekDates: string[]) {
|
||||
this.weekDates = weekDates;
|
||||
this.tracks = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate layout for all events using clean day-based logic
|
||||
*/
|
||||
public calculateLayout(events: CalendarEvent[]): EventLayout[] {
|
||||
|
||||
let layouts: EventLayout[] = [];
|
||||
// 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: EventLayout = {
|
||||
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)
|
||||
*/
|
||||
private findAvailableTrack(startDay: number, endDay: number): number {
|
||||
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)
|
||||
*/
|
||||
private isTrackAvailable(trackIndex: number, startDay: number, endDay: number): boolean {
|
||||
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)
|
||||
*/
|
||||
private getEventStartDay(event: CalendarEvent): number {
|
||||
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)
|
||||
*/
|
||||
private getEventEndDay(event: CalendarEvent): number {
|
||||
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
|
||||
*/
|
||||
private isEventVisible(event: CalendarEvent): boolean {
|
||||
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
|
||||
*/
|
||||
private formatDate(date: Date): string {
|
||||
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}`;
|
||||
}
|
||||
import { ICalendarEvent } from '../types/CalendarTypes';
|
||||
|
||||
export interface IEventLayout {
|
||||
calenderEvent: ICalendarEvent;
|
||||
gridArea: string; // "row-start / col-start / row-end / col-end"
|
||||
startColumn: number;
|
||||
endColumn: number;
|
||||
row: number;
|
||||
columnSpan: number;
|
||||
}
|
||||
|
||||
export class AllDayLayoutEngine {
|
||||
private weekDates: string[];
|
||||
private tracks: boolean[][];
|
||||
|
||||
constructor(weekDates: string[]) {
|
||||
this.weekDates = weekDates;
|
||||
this.tracks = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate layout for all events using clean day-based logic
|
||||
*/
|
||||
public calculateLayout(events: ICalendarEvent[]): IEventLayout[] {
|
||||
|
||||
let layouts: IEventLayout[] = [];
|
||||
// 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: IEventLayout = {
|
||||
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)
|
||||
*/
|
||||
private findAvailableTrack(startDay: number, endDay: number): number {
|
||||
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)
|
||||
*/
|
||||
private isTrackAvailable(trackIndex: number, startDay: number, endDay: number): boolean {
|
||||
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)
|
||||
*/
|
||||
private getEventStartDay(event: ICalendarEvent): number {
|
||||
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)
|
||||
*/
|
||||
private getEventEndDay(event: ICalendarEvent): number {
|
||||
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
|
||||
*/
|
||||
private isEventVisible(event: ICalendarEvent): boolean {
|
||||
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
|
||||
*/
|
||||
private formatDate(date: Date): string {
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,118 +1,118 @@
|
|||
/**
|
||||
* ColumnDetectionUtils - Shared utility for column detection and caching
|
||||
* Used by both DragDropManager and AllDayManager for consistent column detection
|
||||
*/
|
||||
|
||||
import { MousePosition } from "../types/DragDropTypes";
|
||||
|
||||
|
||||
export interface ColumnBounds {
|
||||
date: string;
|
||||
left: number;
|
||||
right: number;
|
||||
boundingClientRect: DOMRect,
|
||||
element : HTMLElement,
|
||||
index: number
|
||||
}
|
||||
|
||||
export class ColumnDetectionUtils {
|
||||
private static columnBoundsCache: ColumnBounds[] = [];
|
||||
|
||||
/**
|
||||
* Update column bounds cache for coordinate-based column detection
|
||||
*/
|
||||
public static updateColumnBoundsCache(): void {
|
||||
// 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 as HTMLElement).dataset.date;
|
||||
|
||||
if (date) {
|
||||
this.columnBoundsCache.push({
|
||||
boundingClientRect : rect,
|
||||
element: column as HTMLElement,
|
||||
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
|
||||
*/
|
||||
public static getColumnBounds(position: MousePosition): ColumnBounds | null{
|
||||
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
|
||||
*/
|
||||
public static getColumnBoundsByDate(date: Date): ColumnBounds | null {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
public static getColumns(): ColumnBounds[] {
|
||||
return [...this.columnBoundsCache];
|
||||
}
|
||||
public static getHeaderColumns(): ColumnBounds[] {
|
||||
|
||||
let dayHeaders: ColumnBounds[] = [];
|
||||
|
||||
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 as HTMLElement).dataset.date;
|
||||
|
||||
if (date) {
|
||||
dayHeaders.push({
|
||||
boundingClientRect : rect,
|
||||
element: column as HTMLElement,
|
||||
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 - 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 class ColumnDetectionUtils {
|
||||
private static columnBoundsCache: IColumnBounds[] = [];
|
||||
|
||||
/**
|
||||
* Update column bounds cache for coordinate-based column detection
|
||||
*/
|
||||
public static updateColumnBoundsCache(): void {
|
||||
// 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 as HTMLElement).dataset.date;
|
||||
|
||||
if (date) {
|
||||
this.columnBoundsCache.push({
|
||||
boundingClientRect : rect,
|
||||
element: column as HTMLElement,
|
||||
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
|
||||
*/
|
||||
public static getColumnBounds(position: IMousePosition): IColumnBounds | null{
|
||||
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
|
||||
*/
|
||||
public static getColumnBoundsByDate(date: Date): IColumnBounds | null {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
public static getColumns(): IColumnBounds[] {
|
||||
return [...this.columnBoundsCache];
|
||||
}
|
||||
public static getHeaderColumns(): IColumnBounds[] {
|
||||
|
||||
let dayHeaders: IColumnBounds[] = [];
|
||||
|
||||
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 as HTMLElement).dataset.date;
|
||||
|
||||
if (date) {
|
||||
dayHeaders.push({
|
||||
boundingClientRect : rect,
|
||||
element: column as HTMLElement,
|
||||
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;
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -1,498 +1,498 @@
|
|||
/**
|
||||
* DateService - Unified date/time service using date-fns
|
||||
* Handles all date operations, timezone conversions, and formatting
|
||||
*/
|
||||
|
||||
import {
|
||||
format,
|
||||
parse,
|
||||
addMinutes,
|
||||
differenceInMinutes,
|
||||
startOfDay,
|
||||
endOfDay,
|
||||
setHours,
|
||||
setMinutes as setMins,
|
||||
getHours,
|
||||
getMinutes,
|
||||
parseISO,
|
||||
isValid,
|
||||
addDays,
|
||||
startOfWeek,
|
||||
endOfWeek,
|
||||
addWeeks,
|
||||
addMonths,
|
||||
isSameDay,
|
||||
getISOWeek
|
||||
} from 'date-fns';
|
||||
import {
|
||||
toZonedTime,
|
||||
fromZonedTime,
|
||||
formatInTimeZone
|
||||
} from 'date-fns-tz';
|
||||
import { CalendarConfig } from '../core/CalendarConfig';
|
||||
|
||||
export class DateService {
|
||||
private timezone: string;
|
||||
|
||||
constructor(config: CalendarConfig) {
|
||||
this.timezone = config.getTimezone();
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// CORE CONVERSIONS
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Convert local date to UTC ISO string
|
||||
* @param localDate - Date in local timezone
|
||||
* @returns ISO string in UTC (with 'Z' suffix)
|
||||
*/
|
||||
public toUTC(localDate: Date): string {
|
||||
return fromZonedTime(localDate, this.timezone).toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert UTC ISO string to local date
|
||||
* @param utcString - ISO string in UTC
|
||||
* @returns Date in local timezone
|
||||
*/
|
||||
public fromUTC(utcString: string): Date {
|
||||
return toZonedTime(parseISO(utcString), this.timezone);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 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
|
||||
*/
|
||||
public formatTime(date: Date, showSeconds = false): string {
|
||||
const pattern = showSeconds ? 'HH:mm:ss' : 'HH:mm';
|
||||
return format(date, pattern);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time range as "HH:mm - HH:mm"
|
||||
* @param start - Start date
|
||||
* @param end - End date
|
||||
* @returns Formatted time range
|
||||
*/
|
||||
public formatTimeRange(start: Date, end: Date): string {
|
||||
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
|
||||
*/
|
||||
public formatTechnicalDateTime(date: Date): string {
|
||||
return format(date, 'yyyy-MM-dd HH:mm:ss');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date as yyyy-MM-dd
|
||||
* @param date - Date to format
|
||||
* @returns ISO date string
|
||||
*/
|
||||
public formatDate(date: Date): string {
|
||||
return format(date, '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
|
||||
*/
|
||||
public formatMonthYear(date: Date, locale: string = 'en-US'): string {
|
||||
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
|
||||
*/
|
||||
public formatISODate(date: Date): string {
|
||||
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")
|
||||
*/
|
||||
public formatTime12(date: Date): string {
|
||||
const hours = getHours(date);
|
||||
const minutes = getMinutes(date);
|
||||
const period = hours >= 12 ? 'PM' : 'AM';
|
||||
const displayHours = hours % 12 || 12;
|
||||
|
||||
return `${displayHours}:${String(minutes).padStart(2, '0')} ${period}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public getDayName(date: Date, format: 'short' | 'long' = 'short', locale: string = 'da-DK'): string {
|
||||
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
|
||||
*/
|
||||
public formatDateRange(
|
||||
start: Date,
|
||||
end: Date,
|
||||
options: {
|
||||
locale?: string;
|
||||
month?: 'numeric' | '2-digit' | 'long' | 'short' | 'narrow';
|
||||
day?: 'numeric' | '2-digit';
|
||||
year?: 'numeric' | '2-digit';
|
||||
} = {}
|
||||
): string {
|
||||
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
|
||||
*/
|
||||
public timeToMinutes(timeString: string): number {
|
||||
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
|
||||
*/
|
||||
public minutesToTime(totalMinutes: number): string {
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
const minutes = totalMinutes % 60;
|
||||
const date = setMins(setHours(new Date(), hours), minutes);
|
||||
return format(date, 'HH:mm');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time from total minutes (alias for minutesToTime)
|
||||
* @param totalMinutes - Minutes since midnight
|
||||
* @returns Time string in format HH:mm
|
||||
*/
|
||||
public formatTimeFromMinutes(totalMinutes: number): string {
|
||||
return this.minutesToTime(totalMinutes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get minutes since midnight for a given date
|
||||
* @param date - Date to calculate from
|
||||
* @returns Minutes since midnight
|
||||
*/
|
||||
public getMinutesSinceMidnight(date: Date): number {
|
||||
return getHours(date) * 60 + getMinutes(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public getDurationMinutes(start: Date | string, end: Date | string): number {
|
||||
const startDate = typeof start === 'string' ? parseISO(start) : start;
|
||||
const endDate = typeof end === 'string' ? parseISO(end) : end;
|
||||
return differenceInMinutes(endDate, startDate);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// WEEK OPERATIONS
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Get start and end of week (Monday to Sunday)
|
||||
* @param date - Reference date
|
||||
* @returns Object with start and end dates
|
||||
*/
|
||||
public getWeekBounds(date: Date): { start: Date; end: Date } {
|
||||
return {
|
||||
start: startOfWeek(date, { weekStartsOn: 1 }), // Monday
|
||||
end: endOfWeek(date, { weekStartsOn: 1 }) // Sunday
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Add weeks to a date
|
||||
* @param date - Base date
|
||||
* @param weeks - Number of weeks to add (can be negative)
|
||||
* @returns New date
|
||||
*/
|
||||
public addWeeks(date: Date, weeks: number): Date {
|
||||
return addWeeks(date, weeks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add months to a date
|
||||
* @param date - Base date
|
||||
* @param months - Number of months to add (can be negative)
|
||||
* @returns New date
|
||||
*/
|
||||
public addMonths(date: Date, months: number): Date {
|
||||
return addMonths(date, months);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ISO week number (1-53)
|
||||
* @param date - Date to get week number for
|
||||
* @returns ISO week number
|
||||
*/
|
||||
public getWeekNumber(date: Date): number {
|
||||
return getISOWeek(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public getFullWeekDates(weekStart: Date): Date[] {
|
||||
const dates: Date[] = [];
|
||||
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
|
||||
*/
|
||||
public getWorkWeekDates(weekStart: Date, workDays: number[]): Date[] {
|
||||
const dates: Date[] = [];
|
||||
|
||||
// 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
|
||||
*/
|
||||
public createDateAtTime(baseDate: Date, totalMinutes: number): Date {
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
const minutes = totalMinutes % 60;
|
||||
return setMins(setHours(startOfDay(baseDate), hours), minutes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Snap date to nearest interval
|
||||
* @param date - Date to snap
|
||||
* @param intervalMinutes - Snap interval in minutes
|
||||
* @returns Snapped date
|
||||
*/
|
||||
public snapToInterval(date: Date, intervalMinutes: number): Date {
|
||||
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
|
||||
*/
|
||||
public isSameDay(date1: Date, date2: Date): boolean {
|
||||
return isSameDay(date1, date2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get start of day
|
||||
* @param date - Date
|
||||
* @returns Start of day (00:00:00)
|
||||
*/
|
||||
public startOfDay(date: Date): Date {
|
||||
return startOfDay(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get end of day
|
||||
* @param date - Date
|
||||
* @returns End of day (23:59:59.999)
|
||||
*/
|
||||
public endOfDay(date: Date): Date {
|
||||
return endOfDay(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add days to a date
|
||||
* @param date - Base date
|
||||
* @param days - Number of days to add (can be negative)
|
||||
* @returns New date
|
||||
*/
|
||||
public addDays(date: Date, days: number): Date {
|
||||
return addDays(date, days);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add minutes to a date
|
||||
* @param date - Base date
|
||||
* @param minutes - Number of minutes to add (can be negative)
|
||||
* @returns New date
|
||||
*/
|
||||
public addMinutes(date: Date, minutes: number): Date {
|
||||
return addMinutes(date, minutes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse ISO string to date
|
||||
* @param isoString - ISO date string
|
||||
* @returns Parsed date
|
||||
*/
|
||||
public parseISO(isoString: string): Date {
|
||||
return parseISO(isoString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if date is valid
|
||||
* @param date - Date to check
|
||||
* @returns True if valid
|
||||
*/
|
||||
public isValid(date: Date): boolean {
|
||||
return isValid(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate date range (start must be before or equal to end)
|
||||
* @param start - Start date
|
||||
* @param end - End date
|
||||
* @returns True if valid range
|
||||
*/
|
||||
public isValidRange(start: Date, end: Date): boolean {
|
||||
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
|
||||
*/
|
||||
public isWithinBounds(date: Date): boolean {
|
||||
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
|
||||
*/
|
||||
public validateDate(
|
||||
date: Date,
|
||||
options: {
|
||||
requireFuture?: boolean;
|
||||
requirePast?: boolean;
|
||||
minDate?: Date;
|
||||
maxDate?: Date;
|
||||
} = {}
|
||||
): { valid: boolean; error?: string } {
|
||||
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 };
|
||||
}
|
||||
/**
|
||||
* DateService - Unified date/time service using date-fns
|
||||
* Handles all date operations, timezone conversions, and formatting
|
||||
*/
|
||||
|
||||
import {
|
||||
format,
|
||||
parse,
|
||||
addMinutes,
|
||||
differenceInMinutes,
|
||||
startOfDay,
|
||||
endOfDay,
|
||||
setHours,
|
||||
setMinutes as setMins,
|
||||
getHours,
|
||||
getMinutes,
|
||||
parseISO,
|
||||
isValid,
|
||||
addDays,
|
||||
startOfWeek,
|
||||
endOfWeek,
|
||||
addWeeks,
|
||||
addMonths,
|
||||
isSameDay,
|
||||
getISOWeek
|
||||
} from 'date-fns';
|
||||
import {
|
||||
toZonedTime,
|
||||
fromZonedTime,
|
||||
formatInTimeZone
|
||||
} from 'date-fns-tz';
|
||||
import { Configuration } from '../configuration/CalendarConfig';
|
||||
|
||||
export class DateService {
|
||||
private timezone: string;
|
||||
|
||||
constructor(config: Configuration) {
|
||||
this.timezone = config.getTimezone();
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// CORE CONVERSIONS
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Convert local date to UTC ISO string
|
||||
* @param localDate - Date in local timezone
|
||||
* @returns ISO string in UTC (with 'Z' suffix)
|
||||
*/
|
||||
public toUTC(localDate: Date): string {
|
||||
return fromZonedTime(localDate, this.timezone).toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert UTC ISO string to local date
|
||||
* @param utcString - ISO string in UTC
|
||||
* @returns Date in local timezone
|
||||
*/
|
||||
public fromUTC(utcString: string): Date {
|
||||
return toZonedTime(parseISO(utcString), this.timezone);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 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
|
||||
*/
|
||||
public formatTime(date: Date, showSeconds = false): string {
|
||||
const pattern = showSeconds ? 'HH:mm:ss' : 'HH:mm';
|
||||
return format(date, pattern);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time range as "HH:mm - HH:mm"
|
||||
* @param start - Start date
|
||||
* @param end - End date
|
||||
* @returns Formatted time range
|
||||
*/
|
||||
public formatTimeRange(start: Date, end: Date): string {
|
||||
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
|
||||
*/
|
||||
public formatTechnicalDateTime(date: Date): string {
|
||||
return format(date, 'yyyy-MM-dd HH:mm:ss');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date as yyyy-MM-dd
|
||||
* @param date - Date to format
|
||||
* @returns ISO date string
|
||||
*/
|
||||
public formatDate(date: Date): string {
|
||||
return format(date, '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
|
||||
*/
|
||||
public formatMonthYear(date: Date, locale: string = 'en-US'): string {
|
||||
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
|
||||
*/
|
||||
public formatISODate(date: Date): string {
|
||||
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")
|
||||
*/
|
||||
public formatTime12(date: Date): string {
|
||||
const hours = getHours(date);
|
||||
const minutes = getMinutes(date);
|
||||
const period = hours >= 12 ? 'PM' : 'AM';
|
||||
const displayHours = hours % 12 || 12;
|
||||
|
||||
return `${displayHours}:${String(minutes).padStart(2, '0')} ${period}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public getDayName(date: Date, format: 'short' | 'long' = 'short', locale: string = 'da-DK'): string {
|
||||
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
|
||||
*/
|
||||
public formatDateRange(
|
||||
start: Date,
|
||||
end: Date,
|
||||
options: {
|
||||
locale?: string;
|
||||
month?: 'numeric' | '2-digit' | 'long' | 'short' | 'narrow';
|
||||
day?: 'numeric' | '2-digit';
|
||||
year?: 'numeric' | '2-digit';
|
||||
} = {}
|
||||
): string {
|
||||
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
|
||||
*/
|
||||
public timeToMinutes(timeString: string): number {
|
||||
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
|
||||
*/
|
||||
public minutesToTime(totalMinutes: number): string {
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
const minutes = totalMinutes % 60;
|
||||
const date = setMins(setHours(new Date(), hours), minutes);
|
||||
return format(date, 'HH:mm');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time from total minutes (alias for minutesToTime)
|
||||
* @param totalMinutes - Minutes since midnight
|
||||
* @returns Time string in format HH:mm
|
||||
*/
|
||||
public formatTimeFromMinutes(totalMinutes: number): string {
|
||||
return this.minutesToTime(totalMinutes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get minutes since midnight for a given date
|
||||
* @param date - Date to calculate from
|
||||
* @returns Minutes since midnight
|
||||
*/
|
||||
public getMinutesSinceMidnight(date: Date): number {
|
||||
return getHours(date) * 60 + getMinutes(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public getDurationMinutes(start: Date | string, end: Date | string): number {
|
||||
const startDate = typeof start === 'string' ? parseISO(start) : start;
|
||||
const endDate = typeof end === 'string' ? parseISO(end) : end;
|
||||
return differenceInMinutes(endDate, startDate);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// WEEK OPERATIONS
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Get start and end of week (Monday to Sunday)
|
||||
* @param date - Reference date
|
||||
* @returns Object with start and end dates
|
||||
*/
|
||||
public getWeekBounds(date: Date): { start: Date; end: Date } {
|
||||
return {
|
||||
start: startOfWeek(date, { weekStartsOn: 1 }), // Monday
|
||||
end: endOfWeek(date, { weekStartsOn: 1 }) // Sunday
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Add weeks to a date
|
||||
* @param date - Base date
|
||||
* @param weeks - Number of weeks to add (can be negative)
|
||||
* @returns New date
|
||||
*/
|
||||
public addWeeks(date: Date, weeks: number): Date {
|
||||
return addWeeks(date, weeks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add months to a date
|
||||
* @param date - Base date
|
||||
* @param months - Number of months to add (can be negative)
|
||||
* @returns New date
|
||||
*/
|
||||
public addMonths(date: Date, months: number): Date {
|
||||
return addMonths(date, months);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ISO week number (1-53)
|
||||
* @param date - Date to get week number for
|
||||
* @returns ISO week number
|
||||
*/
|
||||
public getWeekNumber(date: Date): number {
|
||||
return getISOWeek(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public getFullWeekDates(weekStart: Date): Date[] {
|
||||
const dates: Date[] = [];
|
||||
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
|
||||
*/
|
||||
public getWorkWeekDates(weekStart: Date, workDays: number[]): Date[] {
|
||||
const dates: Date[] = [];
|
||||
|
||||
// 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
|
||||
*/
|
||||
public createDateAtTime(baseDate: Date, totalMinutes: number): Date {
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
const minutes = totalMinutes % 60;
|
||||
return setMins(setHours(startOfDay(baseDate), hours), minutes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Snap date to nearest interval
|
||||
* @param date - Date to snap
|
||||
* @param intervalMinutes - Snap interval in minutes
|
||||
* @returns Snapped date
|
||||
*/
|
||||
public snapToInterval(date: Date, intervalMinutes: number): Date {
|
||||
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
|
||||
*/
|
||||
public isSameDay(date1: Date, date2: Date): boolean {
|
||||
return isSameDay(date1, date2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get start of day
|
||||
* @param date - Date
|
||||
* @returns Start of day (00:00:00)
|
||||
*/
|
||||
public startOfDay(date: Date): Date {
|
||||
return startOfDay(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get end of day
|
||||
* @param date - Date
|
||||
* @returns End of day (23:59:59.999)
|
||||
*/
|
||||
public endOfDay(date: Date): Date {
|
||||
return endOfDay(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add days to a date
|
||||
* @param date - Base date
|
||||
* @param days - Number of days to add (can be negative)
|
||||
* @returns New date
|
||||
*/
|
||||
public addDays(date: Date, days: number): Date {
|
||||
return addDays(date, days);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add minutes to a date
|
||||
* @param date - Base date
|
||||
* @param minutes - Number of minutes to add (can be negative)
|
||||
* @returns New date
|
||||
*/
|
||||
public addMinutes(date: Date, minutes: number): Date {
|
||||
return addMinutes(date, minutes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse ISO string to date
|
||||
* @param isoString - ISO date string
|
||||
* @returns Parsed date
|
||||
*/
|
||||
public parseISO(isoString: string): Date {
|
||||
return parseISO(isoString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if date is valid
|
||||
* @param date - Date to check
|
||||
* @returns True if valid
|
||||
*/
|
||||
public isValid(date: Date): boolean {
|
||||
return isValid(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate date range (start must be before or equal to end)
|
||||
* @param start - Start date
|
||||
* @param end - End date
|
||||
* @returns True if valid range
|
||||
*/
|
||||
public isValidRange(start: Date, end: Date): boolean {
|
||||
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
|
||||
*/
|
||||
public isWithinBounds(date: Date): boolean {
|
||||
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
|
||||
*/
|
||||
public validateDate(
|
||||
date: Date,
|
||||
options: {
|
||||
requireFuture?: boolean;
|
||||
requirePast?: boolean;
|
||||
minDate?: Date;
|
||||
maxDate?: Date;
|
||||
} = {}
|
||||
): { valid: boolean; error?: string } {
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { CalendarConfig } from '../core/CalendarConfig';
|
||||
import { ColumnBounds } from './ColumnDetectionUtils';
|
||||
import { Configuration } from '../configuration/CalendarConfig';
|
||||
import { IColumnBounds } from './ColumnDetectionUtils';
|
||||
import { DateService } from './DateService';
|
||||
import { TimeFormatter } from './TimeFormatter';
|
||||
|
||||
|
|
@ -11,9 +11,9 @@ import { TimeFormatter } from './TimeFormatter';
|
|||
*/
|
||||
export class PositionUtils {
|
||||
private dateService: DateService;
|
||||
private config: CalendarConfig;
|
||||
private config: Configuration;
|
||||
|
||||
constructor(dateService: DateService, config: CalendarConfig) {
|
||||
constructor(dateService: DateService, config: Configuration) {
|
||||
this.dateService = dateService;
|
||||
this.config = config;
|
||||
}
|
||||
|
|
@ -169,7 +169,7 @@ export class PositionUtils {
|
|||
/**
|
||||
* Beregn Y position fra mouse/touch koordinat
|
||||
*/
|
||||
public getPositionFromCoordinate(clientY: number, column: ColumnBounds): number {
|
||||
public getPositionFromCoordinate(clientY: number, column: IColumnBounds): number {
|
||||
|
||||
const relativeY = clientY - column.boundingClientRect.top;
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
import { DateService } from './DateService';
|
||||
|
||||
export interface TimeFormatSettings {
|
||||
export interface ITimeFormatSettings {
|
||||
timezone: string;
|
||||
use24HourFormat: boolean;
|
||||
locale: string;
|
||||
|
|
@ -19,7 +19,7 @@ export interface TimeFormatSettings {
|
|||
}
|
||||
|
||||
export class TimeFormatter {
|
||||
private static settings: TimeFormatSettings = {
|
||||
private static settings: ITimeFormatSettings = {
|
||||
timezone: 'Europe/Copenhagen', // Default to Denmark
|
||||
use24HourFormat: true, // 24-hour format standard in Denmark
|
||||
locale: 'da-DK', // Danish locale
|
||||
|
|
@ -44,7 +44,7 @@ export class TimeFormatter {
|
|||
/**
|
||||
* Configure time formatting settings
|
||||
*/
|
||||
static configure(settings: Partial<TimeFormatSettings>): void {
|
||||
static configure(settings: Partial<ITimeFormatSettings>): void {
|
||||
TimeFormatter.settings = { ...TimeFormatter.settings, ...settings };
|
||||
// Reset DateService to pick up new timezone
|
||||
TimeFormatter.dateService = null;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue