Improves date handling and event stacking

Enhances date validation and timezone handling using DateService, ensuring data integrity and consistency.

Refactors event rendering and dragging to correctly handle date transformations.

Adds a test plan for event stacking and z-index management.

Fixes edge cases in navigation and date calculations for week/year boundaries and DST transitions.
This commit is contained in:
Janus C. H. Knudsen 2025-10-04 00:32:26 +02:00
parent a86a736340
commit 9bc082eed4
20 changed files with 1641 additions and 41 deletions

View file

@ -1,7 +1,7 @@
// All-day row height management and animations
import { eventBus } from '../core/EventBus';
import { ALL_DAY_CONSTANTS } from '../core/CalendarConfig';
import { ALL_DAY_CONSTANTS, calendarConfig } from '../core/CalendarConfig';
import { AllDayEventRenderer } from '../renderers/AllDayEventRenderer';
import { AllDayLayoutEngine, EventLayout } from '../utils/AllDayLayoutEngine';
import { ColumnBounds, ColumnDetectionUtils } from '../utils/ColumnDetectionUtils';
@ -18,6 +18,7 @@ import { DragOffset, MousePosition } from '../types/DragDropTypes';
import { CoreEvents } from '../constants/CoreEvents';
import { EventManager } from './EventManager';
import { differenceInCalendarDays } from 'date-fns';
import { DateService } from '../utils/DateService';
/**
* AllDayManager - Handles all-day row height animations and management
@ -26,6 +27,7 @@ import { differenceInCalendarDays } from 'date-fns';
export class AllDayManager {
private allDayEventRenderer: AllDayEventRenderer;
private eventManager: EventManager;
private dateService: DateService;
private layoutEngine: AllDayLayoutEngine | null = null;
@ -43,6 +45,8 @@ export class AllDayManager {
constructor(eventManager: EventManager) {
this.eventManager = eventManager;
this.allDayEventRenderer = new AllDayEventRenderer();
const timezone = calendarConfig.getTimezone?.();
this.dateService = new DateService(timezone);
// Sync CSS variable with TypeScript constant to ensure consistency
document.documentElement.style.setProperty('--single-row-height', `${ALL_DAY_CONSTANTS.EVENT_HEIGHT}px`);
@ -420,9 +424,9 @@ export class AllDayManager {
newEndDate.setDate(newEndDate.getDate() + durationDays);
newEndDate.setHours(originalEndDate.getHours(), originalEndDate.getMinutes(), originalEndDate.getSeconds(), originalEndDate.getMilliseconds());
// Update data attributes with new dates
dragEndEvent.draggedClone.dataset.start = newStartDate.toISOString();
dragEndEvent.draggedClone.dataset.end = newEndDate.toISOString();
// Update data attributes with new dates (convert to UTC)
dragEndEvent.draggedClone.dataset.start = this.dateService.toUTC(newStartDate);
dragEndEvent.draggedClone.dataset.end = this.dateService.toUTC(newEndDate);
const droppedEvent: CalendarEvent = {
id: eventId,

View file

@ -42,7 +42,7 @@ export class CalendarManager {
this.eventRenderer = eventRenderer;
this.scrollManager = scrollManager;
this.eventFilterManager = new EventFilterManager();
const timezone = calendarConfig.getTimezone?.() || 'Europe/Copenhagen';
const timezone = calendarConfig.getTimezone?.();
this.dateService = new DateService(timezone);
this.setupEventListeners();
}

View file

@ -30,7 +30,7 @@ export class EventManager {
constructor(eventBus: IEventBus) {
this.eventBus = eventBus;
const timezone = calendarConfig.getTimezone?.() || 'Europe/Copenhagen';
const timezone = calendarConfig.getTimezone?.();
this.dateService = new DateService(timezone);
}
@ -156,20 +156,23 @@ export class EventManager {
return null;
}
try {
if (isNaN(event.start.getTime())) {
console.warn(`EventManager: Invalid event start date for event ${id}:`, event.start);
return null;
}
return {
event,
eventDate: event.start
};
} catch (error) {
console.warn(`EventManager: Failed to parse event date for event ${id}:`, error);
// Validate event dates
const validation = this.dateService.validateDate(event.start);
if (!validation.valid) {
console.warn(`EventManager: Invalid event start date for event ${id}:`, validation.error);
return null;
}
// Validate date range
if (!this.dateService.isValidRange(event.start, event.end)) {
console.warn(`EventManager: Invalid date range for event ${id}: start must be before end`);
return null;
}
return {
event,
eventDate: event.start
};
}
/**

View file

@ -137,7 +137,7 @@ export class GridManager {
const weekEnd = this.getWeekEnd(this.currentDate);
return this.dateService.formatDateRange(weekStart, weekEnd);
case 'month':
return this.currentDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
return this.dateService.formatMonthYear(this.currentDate);
default:
const defaultWeekStart = this.getISOWeekStart(this.currentDate);
const defaultWeekEnd = this.getWeekEnd(this.currentDate);

View file

@ -90,17 +90,22 @@ export class NavigationManager {
this.eventBus.on(CoreEvents.DATE_CHANGED, (event: Event) => {
const customEvent = event as CustomEvent;
const dateFromEvent = customEvent.detail.currentDate;
// Validate date before processing
if (!dateFromEvent) {
console.warn('NavigationManager: No date provided in DATE_CHANGED event');
return;
}
const targetDate = new Date(dateFromEvent);
if (isNaN(targetDate.getTime())) {
// Use DateService validation
const validation = this.dateService.validateDate(targetDate);
if (!validation.valid) {
console.warn('NavigationManager: Invalid date received:', validation.error);
return;
}
this.navigateToDate(targetDate);
});

View file

@ -38,7 +38,7 @@ export class WorkHoursManager {
private workSchedule: WorkScheduleConfig;
constructor() {
const timezone = calendarConfig.getTimezone?.() || 'Europe/Copenhagen';
const timezone = calendarConfig.getTimezone?.();
this.dateService = new DateService(timezone);
// Default work schedule - will be loaded from JSON later