Initial commit: Calendar Plantempus project setup with TypeScript, ASP.NET Core, and event-driven architecture
This commit is contained in:
commit
f06c02121c
38 changed files with 8233 additions and 0 deletions
230
src/utils/DateUtils.ts
Normal file
230
src/utils/DateUtils.ts
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
// Date and time utility functions
|
||||
|
||||
/**
|
||||
* Date and time utility functions
|
||||
*/
|
||||
export class DateUtils {
|
||||
/**
|
||||
* Get start of week for a given date
|
||||
*/
|
||||
static getWeekStart(date: Date, firstDayOfWeek: number = 1): Date {
|
||||
const d = new Date(date);
|
||||
const day = d.getDay();
|
||||
const diff = (day - firstDayOfWeek + 7) % 7;
|
||||
d.setDate(d.getDate() - diff);
|
||||
d.setHours(0, 0, 0, 0);
|
||||
return d;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get end of week for a given date
|
||||
*/
|
||||
static getWeekEnd(date: Date, firstDayOfWeek: number = 1): Date {
|
||||
const start = this.getWeekStart(date, firstDayOfWeek);
|
||||
const end = new Date(start);
|
||||
end.setDate(end.getDate() + 6);
|
||||
end.setHours(23, 59, 59, 999);
|
||||
return end;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date to YYYY-MM-DD
|
||||
*/
|
||||
static formatDate(date: Date): string {
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time to HH:MM
|
||||
*/
|
||||
static formatTime(date: Date): string {
|
||||
return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time to 12-hour format
|
||||
*/
|
||||
static formatTime12(date: Date): string {
|
||||
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
|
||||
*/
|
||||
static minutesToTime(minutes: number): string {
|
||||
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
|
||||
*/
|
||||
static timeToMinutes(timeStr: string): number {
|
||||
const [time] = timeStr.split('T').pop()!.split('.');
|
||||
const [hours, minutes] = time.split(':').map(Number);
|
||||
return hours * 60 + minutes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get minutes since start of day
|
||||
*/
|
||||
static getMinutesSinceMidnight(date: Date | string): number {
|
||||
const d = typeof date === 'string' ? new Date(date) : date;
|
||||
return d.getHours() * 60 + d.getMinutes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate duration in minutes between two dates
|
||||
*/
|
||||
static getDurationMinutes(start: Date | string, end: Date | string): number {
|
||||
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 date is today
|
||||
*/
|
||||
static isToday(date: Date): boolean {
|
||||
const today = new Date();
|
||||
return date.toDateString() === today.toDateString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two dates are on the same day
|
||||
*/
|
||||
static isSameDay(date1: Date, date2: Date): boolean {
|
||||
return date1.toDateString() === date2.toDateString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if event spans multiple days
|
||||
*/
|
||||
static isMultiDay(start: Date | string, end: Date | string): boolean {
|
||||
const startDate = typeof start === 'string' ? new Date(start) : start;
|
||||
const endDate = typeof end === 'string' ? new Date(end) : end;
|
||||
return !this.isSameDay(startDate, endDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get day name
|
||||
*/
|
||||
static getDayName(date: Date, format: 'short' | 'long' = 'short'): string {
|
||||
const days = {
|
||||
short: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
|
||||
long: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
|
||||
};
|
||||
return days[format][date.getDay()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add days to date
|
||||
*/
|
||||
static addDays(date: Date, days: number): Date {
|
||||
const result = new Date(date);
|
||||
result.setDate(result.getDate() + days);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add minutes to date
|
||||
*/
|
||||
static addMinutes(date: Date, minutes: number): Date {
|
||||
const result = new Date(date);
|
||||
result.setMinutes(result.getMinutes() + minutes);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Snap time to nearest interval
|
||||
*/
|
||||
static snapToInterval(date: Date, intervalMinutes: number): Date {
|
||||
const minutes = date.getMinutes();
|
||||
const snappedMinutes = Math.round(minutes / intervalMinutes) * intervalMinutes;
|
||||
const result = new Date(date);
|
||||
result.setMinutes(snappedMinutes);
|
||||
result.setSeconds(0);
|
||||
result.setMilliseconds(0);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current time in minutes since day start
|
||||
*/
|
||||
static getCurrentTimeMinutes(dayStartHour: number = 0): number {
|
||||
const now = new Date();
|
||||
const minutesSinceMidnight = now.getHours() * 60 + now.getMinutes();
|
||||
return minutesSinceMidnight - (dayStartHour * 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format duration to human readable string
|
||||
*/
|
||||
static formatDuration(minutes: number): string {
|
||||
if (minutes < 60) {
|
||||
return `${minutes} min`;
|
||||
}
|
||||
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const mins = minutes % 60;
|
||||
|
||||
if (mins === 0) {
|
||||
return `${hours} hour${hours > 1 ? 's' : ''}`;
|
||||
}
|
||||
|
||||
return `${hours} hour${hours > 1 ? 's' : ''} ${mins} min`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ISO week number for a given date
|
||||
*/
|
||||
static getWeekNumber(date: Date): number {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get month names array
|
||||
*/
|
||||
static getMonthNames(format: 'short' | 'long' = 'short'): string[] {
|
||||
const months = {
|
||||
short: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
|
||||
long: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
|
||||
};
|
||||
return months[format];
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date range for display (e.g., "Jan 15 - 21, 2024" or "Jan 15 - Feb 2, 2024")
|
||||
*/
|
||||
static formatDateRange(startDate: Date, endDate: Date): string {
|
||||
const monthNames = this.getMonthNames('short');
|
||||
|
||||
const startMonth = monthNames[startDate.getMonth()];
|
||||
const endMonth = monthNames[endDate.getMonth()];
|
||||
const startDay = startDate.getDate();
|
||||
const endDay = endDate.getDate();
|
||||
const startYear = startDate.getFullYear();
|
||||
const endYear = endDate.getFullYear();
|
||||
|
||||
if (startMonth === endMonth && startYear === endYear) {
|
||||
return `${startMonth} ${startDay} - ${endDay}, ${startYear}`;
|
||||
} else if (startYear !== endYear) {
|
||||
return `${startMonth} ${startDay}, ${startYear} - ${endMonth} ${endDay}, ${endYear}`;
|
||||
} else {
|
||||
return `${startMonth} ${startDay} - ${endMonth} ${endDay}, ${startYear}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
291
src/utils/PositionUtils.ts
Normal file
291
src/utils/PositionUtils.ts
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
import { CalendarConfig } from '../core/CalendarConfig.js';
|
||||
|
||||
/**
|
||||
* PositionUtils - Utility funktioner til pixel/minut konvertering
|
||||
* Håndterer positionering og størrelse beregninger for calendar events
|
||||
*/
|
||||
export class PositionUtils {
|
||||
private config: CalendarConfig;
|
||||
|
||||
constructor(config: CalendarConfig) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Konverter minutter til pixels
|
||||
*/
|
||||
public minutesToPixels(minutes: number): number {
|
||||
const pixelsPerHour = this.config.get('hourHeight');
|
||||
return (minutes / 60) * pixelsPerHour;
|
||||
}
|
||||
|
||||
/**
|
||||
* Konverter pixels til minutter
|
||||
*/
|
||||
public pixelsToMinutes(pixels: number): number {
|
||||
const pixelsPerHour = this.config.get('hourHeight');
|
||||
return (pixels / pixelsPerHour) * 60;
|
||||
}
|
||||
|
||||
/**
|
||||
* Konverter tid (HH:MM) til pixels fra dag start
|
||||
*/
|
||||
public timeToPixels(timeString: string): number {
|
||||
const [hours, minutes] = timeString.split(':').map(Number);
|
||||
const totalMinutes = (hours * 60) + minutes;
|
||||
const dayStartMinutes = this.config.get('dayStartHour') * 60;
|
||||
const minutesFromDayStart = totalMinutes - dayStartMinutes;
|
||||
|
||||
return this.minutesToPixels(minutesFromDayStart);
|
||||
}
|
||||
|
||||
/**
|
||||
* Konverter Date object til pixels fra dag start
|
||||
*/
|
||||
public dateToPixels(date: Date): number {
|
||||
const hours = date.getHours();
|
||||
const minutes = date.getMinutes();
|
||||
const totalMinutes = (hours * 60) + minutes;
|
||||
const dayStartMinutes = this.config.get('dayStartHour') * 60;
|
||||
const minutesFromDayStart = totalMinutes - dayStartMinutes;
|
||||
|
||||
return this.minutesToPixels(minutesFromDayStart);
|
||||
}
|
||||
|
||||
/**
|
||||
* Konverter pixels til tid (HH:MM format)
|
||||
*/
|
||||
public pixelsToTime(pixels: number): string {
|
||||
const minutes = this.pixelsToMinutes(pixels);
|
||||
const dayStartMinutes = this.config.get('dayStartHour') * 60;
|
||||
const totalMinutes = dayStartMinutes + minutes;
|
||||
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
const mins = Math.round(totalMinutes % 60);
|
||||
|
||||
return `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Beregn event position og størrelse
|
||||
*/
|
||||
public calculateEventPosition(startTime: string | Date, endTime: string | Date): {
|
||||
top: number;
|
||||
height: number;
|
||||
duration: number;
|
||||
} {
|
||||
let startPixels: number;
|
||||
let endPixels: number;
|
||||
|
||||
if (typeof startTime === 'string') {
|
||||
startPixels = this.timeToPixels(startTime);
|
||||
} else {
|
||||
startPixels = this.dateToPixels(startTime);
|
||||
}
|
||||
|
||||
if (typeof endTime === 'string') {
|
||||
endPixels = this.timeToPixels(endTime);
|
||||
} else {
|
||||
endPixels = this.dateToPixels(endTime);
|
||||
}
|
||||
|
||||
const height = Math.max(endPixels - startPixels, this.getMinimumEventHeight());
|
||||
const duration = this.pixelsToMinutes(height);
|
||||
|
||||
return {
|
||||
top: startPixels,
|
||||
height,
|
||||
duration
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Snap position til grid interval
|
||||
*/
|
||||
public snapToGrid(pixels: number): number {
|
||||
const snapInterval = this.config.get('snapInterval');
|
||||
const snapPixels = this.minutesToPixels(snapInterval);
|
||||
|
||||
return Math.round(pixels / snapPixels) * snapPixels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Snap tid til interval
|
||||
*/
|
||||
public snapTimeToInterval(timeString: string): string {
|
||||
const [hours, minutes] = timeString.split(':').map(Number);
|
||||
const totalMinutes = (hours * 60) + minutes;
|
||||
const snapInterval = this.config.get('snapInterval');
|
||||
|
||||
const snappedMinutes = Math.round(totalMinutes / snapInterval) * snapInterval;
|
||||
const snappedHours = Math.floor(snappedMinutes / 60);
|
||||
const remainingMinutes = snappedMinutes % 60;
|
||||
|
||||
return `${snappedHours.toString().padStart(2, '0')}:${remainingMinutes.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Beregn kolonne position for overlappende events
|
||||
*/
|
||||
public calculateColumnPosition(eventIndex: number, totalColumns: number, containerWidth: number): {
|
||||
left: number;
|
||||
width: number;
|
||||
} {
|
||||
const columnWidth = containerWidth / totalColumns;
|
||||
const left = eventIndex * columnWidth;
|
||||
|
||||
// Lav lidt margin mellem kolonnerne
|
||||
const margin = 2;
|
||||
const adjustedWidth = columnWidth - margin;
|
||||
|
||||
return {
|
||||
left: left + (margin / 2),
|
||||
width: Math.max(adjustedWidth, 50) // Minimum width
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check om to events overlapper i tid
|
||||
*/
|
||||
public eventsOverlap(
|
||||
start1: string | Date,
|
||||
end1: string | Date,
|
||||
start2: string | Date,
|
||||
end2: string | Date
|
||||
): boolean {
|
||||
const pos1 = this.calculateEventPosition(start1, end1);
|
||||
const pos2 = this.calculateEventPosition(start2, end2);
|
||||
|
||||
const event1End = pos1.top + pos1.height;
|
||||
const event2End = pos2.top + pos2.height;
|
||||
|
||||
return !(event1End <= pos2.top || event2End <= pos1.top);
|
||||
}
|
||||
|
||||
/**
|
||||
* Beregn Y position fra mouse/touch koordinat
|
||||
*/
|
||||
public getPositionFromCoordinate(clientY: number, containerElement: HTMLElement): number {
|
||||
const rect = containerElement.getBoundingClientRect();
|
||||
const relativeY = clientY - rect.top;
|
||||
|
||||
// Snap til grid
|
||||
return this.snapToGrid(relativeY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Beregn tid fra mouse/touch koordinat
|
||||
*/
|
||||
public getTimeFromCoordinate(clientY: number, containerElement: HTMLElement): string {
|
||||
const position = this.getPositionFromCoordinate(clientY, containerElement);
|
||||
return this.pixelsToTime(position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Valider at tid er inden for arbejdstimer
|
||||
*/
|
||||
public isWithinWorkHours(timeString: string): boolean {
|
||||
const [hours] = timeString.split(':').map(Number);
|
||||
return hours >= this.config.get('workStartHour') && hours < this.config.get('workEndHour');
|
||||
}
|
||||
|
||||
/**
|
||||
* Valider at tid er inden for dag grænser
|
||||
*/
|
||||
public isWithinDayBounds(timeString: string): boolean {
|
||||
const [hours] = timeString.split(':').map(Number);
|
||||
return hours >= this.config.get('dayStartHour') && hours < this.config.get('dayEndHour');
|
||||
}
|
||||
|
||||
/**
|
||||
* Hent minimum event højde i pixels
|
||||
*/
|
||||
public getMinimumEventHeight(): number {
|
||||
// Minimum 15 minutter
|
||||
return this.minutesToPixels(15);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hent maksimum event højde i pixels (hele dagen)
|
||||
*/
|
||||
public getMaximumEventHeight(): number {
|
||||
const dayDurationHours = this.config.get('dayEndHour') - this.config.get('dayStartHour');
|
||||
return dayDurationHours * this.config.get('hourHeight');
|
||||
}
|
||||
|
||||
/**
|
||||
* Beregn total kalender højde
|
||||
*/
|
||||
public getTotalCalendarHeight(): number {
|
||||
return this.getMaximumEventHeight();
|
||||
}
|
||||
|
||||
/**
|
||||
* Konverter ISO datetime til lokal tid string
|
||||
*/
|
||||
public isoToTimeString(isoString: string): string {
|
||||
const date = new Date(isoString);
|
||||
const hours = date.getHours();
|
||||
const minutes = date.getMinutes();
|
||||
|
||||
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Konverter lokal tid string til ISO datetime for i dag
|
||||
*/
|
||||
public timeStringToIso(timeString: string, date: Date = new Date()): string {
|
||||
const [hours, minutes] = timeString.split(':').map(Number);
|
||||
const newDate = new Date(date);
|
||||
newDate.setHours(hours, minutes, 0, 0);
|
||||
|
||||
return newDate.toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Beregn event varighed i minutter
|
||||
*/
|
||||
public calculateDuration(startTime: string | Date, endTime: string | Date): number {
|
||||
let startMs: number;
|
||||
let endMs: number;
|
||||
|
||||
if (typeof startTime === 'string') {
|
||||
startMs = new Date(startTime).getTime();
|
||||
} else {
|
||||
startMs = startTime.getTime();
|
||||
}
|
||||
|
||||
if (typeof endTime === 'string') {
|
||||
endMs = new Date(endTime).getTime();
|
||||
} else {
|
||||
endMs = endTime.getTime();
|
||||
}
|
||||
|
||||
return Math.round((endMs - startMs) / (1000 * 60)); // Minutter
|
||||
}
|
||||
|
||||
/**
|
||||
* Format varighed til læsbar tekst
|
||||
*/
|
||||
public formatDuration(minutes: number): string {
|
||||
if (minutes < 60) {
|
||||
return `${minutes} min`;
|
||||
}
|
||||
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const remainingMinutes = minutes % 60;
|
||||
|
||||
if (remainingMinutes === 0) {
|
||||
return `${hours} time${hours !== 1 ? 'r' : ''}`;
|
||||
}
|
||||
|
||||
return `${hours}t ${remainingMinutes}m`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opdater konfiguration
|
||||
*/
|
||||
public updateConfig(newConfig: CalendarConfig): void {
|
||||
this.config = newConfig;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue