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:
parent
a86a736340
commit
9bc082eed4
20 changed files with 1641 additions and 41 deletions
|
|
@ -1861,8 +1861,8 @@
|
|||
{
|
||||
"id": "144",
|
||||
"title": "Team Standup",
|
||||
"start": "2025-09-29T05:00:00Z",
|
||||
"end": "2025-09-29T05:30:00Z",
|
||||
"start": "2025-09-29T07:30:00Z",
|
||||
"end": "2025-09-29T08:30:00Z",
|
||||
"type": "meeting",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
|
|
@ -1874,7 +1874,7 @@
|
|||
{
|
||||
"id": "145",
|
||||
"title": "Månedlig Planlægning",
|
||||
"start": "2025-09-29T06:00:00Z",
|
||||
"start": "2025-09-29T07:00:00Z",
|
||||
"end": "2025-09-29T08:00:00Z",
|
||||
"type": "meeting",
|
||||
"allDay": false,
|
||||
|
|
@ -1887,8 +1887,8 @@
|
|||
{
|
||||
"id": "146",
|
||||
"title": "Performance Test",
|
||||
"start": "2025-09-29T10:00:00Z",
|
||||
"end": "2025-09-29T12:00:00Z",
|
||||
"start": "2025-09-29T09:00:00Z",
|
||||
"end": "2025-09-29T10:00:00Z",
|
||||
"type": "work",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { calendarConfig } from '../core/CalendarConfig';
|
|||
import { TimeFormatter } from '../utils/TimeFormatter';
|
||||
import { PositionUtils } from '../utils/PositionUtils';
|
||||
import { EventLayout } from '../utils/AllDayLayoutEngine';
|
||||
import { DateService } from '../utils/DateService';
|
||||
|
||||
/**
|
||||
* Abstract base class for event DOM elements
|
||||
|
|
@ -10,9 +11,12 @@ import { EventLayout } from '../utils/AllDayLayoutEngine';
|
|||
export abstract class BaseEventElement {
|
||||
protected element: HTMLElement;
|
||||
protected event: CalendarEvent;
|
||||
protected dateService: DateService;
|
||||
|
||||
protected constructor(event: CalendarEvent) {
|
||||
this.event = event;
|
||||
const timezone = calendarConfig.getTimezone?.();
|
||||
this.dateService = new DateService(timezone);
|
||||
this.element = this.createElement();
|
||||
this.setDataAttributes();
|
||||
}
|
||||
|
|
@ -28,8 +32,8 @@ export abstract class BaseEventElement {
|
|||
protected setDataAttributes(): void {
|
||||
this.element.dataset.eventId = this.event.id;
|
||||
this.element.dataset.title = this.event.title;
|
||||
this.element.dataset.start = this.event.start.toISOString();
|
||||
this.element.dataset.end = this.event.end.toISOString();
|
||||
this.element.dataset.start = this.dateService.toUTC(this.event.start);
|
||||
this.element.dataset.end = this.dateService.toUTC(this.event.end);
|
||||
this.element.dataset.type = this.event.type;
|
||||
this.element.dataset.duration = this.event.metadata?.duration?.toString() || '60';
|
||||
}
|
||||
|
|
@ -245,8 +249,8 @@ export class SwpAllDayEventElement extends BaseEventElement {
|
|||
*/
|
||||
private setAllDayAttributes(): void {
|
||||
this.element.dataset.allday = "true";
|
||||
this.element.dataset.start = this.event.start.toISOString();
|
||||
this.element.dataset.end = this.event.end.toISOString();
|
||||
this.element.dataset.start = this.dateService.toUTC(this.event.start);
|
||||
this.element.dataset.end = this.dateService.toUTC(this.event.end);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export class DateEventRenderer implements EventRendererStrategy {
|
|||
private dateService: DateService;
|
||||
|
||||
constructor() {
|
||||
const timezone = calendarConfig.getTimezone?.() || 'Europe/Copenhagen';
|
||||
const timezone = calendarConfig.getTimezone?.();
|
||||
this.dateService = new DateService(timezone);
|
||||
this.setupDragEventListeners();
|
||||
}
|
||||
|
|
@ -102,6 +102,7 @@ export class DateEventRenderer implements EventRendererStrategy {
|
|||
|
||||
private applyDragStyling(element: HTMLElement): void {
|
||||
element.classList.add('dragging');
|
||||
element.style.removeProperty("margin-left");
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -174,8 +175,9 @@ export class DateEventRenderer implements EventRendererStrategy {
|
|||
endDate = this.dateService.addDays(endDate, extraDays);
|
||||
}
|
||||
|
||||
element.dataset.start = startDate.toISOString();
|
||||
element.dataset.end = endDate.toISOString();
|
||||
// Convert to UTC before storing as ISO string
|
||||
element.dataset.start = this.dateService.toUTC(startDate);
|
||||
element.dataset.end = this.dateService.toUTC(endDate);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export class GridRenderer {
|
|||
private dateService: DateService;
|
||||
|
||||
constructor() {
|
||||
const timezone = calendarConfig.getTimezone?.() || 'Europe/Copenhagen';
|
||||
const timezone = calendarConfig.getTimezone?.();
|
||||
this.dateService = new DateService(timezone);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export class WeekViewStrategy implements ViewStrategy {
|
|||
private styleManager: GridStyleManager;
|
||||
|
||||
constructor() {
|
||||
const timezone = calendarConfig.getTimezone?.() || 'Europe/Copenhagen';
|
||||
const timezone = calendarConfig.getTimezone?.();
|
||||
this.dateService = new DateService(timezone);
|
||||
this.gridRenderer = new GridRenderer();
|
||||
this.styleManager = new GridStyleManager();
|
||||
|
|
|
|||
|
|
@ -101,6 +101,16 @@ export class DateService {
|
|||
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)
|
||||
|
|
@ -413,6 +423,76 @@ export class DateService {
|
|||
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 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if event spans multiple days
|
||||
|
|
|
|||
|
|
@ -222,12 +222,12 @@ export class PositionUtils {
|
|||
}
|
||||
|
||||
/**
|
||||
* Convert time string to ISO datetime using DateService
|
||||
* Convert time string to ISO datetime using DateService with timezone handling
|
||||
*/
|
||||
public static timeStringToIso(timeString: string, date: Date = new Date()): string {
|
||||
const totalMinutes = PositionUtils.dateService.timeToMinutes(timeString);
|
||||
const newDate = PositionUtils.dateService.createDateAtTime(date, totalMinutes);
|
||||
return newDate.toISOString();
|
||||
return PositionUtils.dateService.toUTC(newDate);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue