import { CalendarEvent } from '../types/CalendarTypes'; import { calendarConfig } from '../core/CalendarConfig'; import { TimeFormatter } from '../utils/TimeFormatter'; import { PositionUtils } from '../utils/PositionUtils'; /** * Abstract base class for event DOM elements */ export abstract class BaseEventElement { protected element: HTMLElement; protected event: CalendarEvent; protected constructor(event: CalendarEvent) { this.event = event; this.element = this.createElement(); this.setDataAttributes(); } /** * Create the underlying DOM element */ protected abstract createElement(): HTMLElement; /** * Set standard data attributes on the element */ 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.type = this.event.type; this.element.dataset.duration = this.event.metadata?.duration?.toString() || '60'; } /** * Get the DOM element */ public getElement(): HTMLElement { return this.element; } /** * Format time for display using TimeFormatter */ protected formatTime(date: Date): string { return TimeFormatter.formatTime(date); } /** * Calculate event position for timed events using PositionUtils */ protected calculateEventPosition(): { top: number; height: number } { return PositionUtils.calculateEventPosition(this.event.start, this.event.end); } } /** * Timed event element (swp-event) */ export class SwpEventElement extends BaseEventElement { private constructor(event: CalendarEvent) { super(event); this.createInnerStructure(); this.applyPositioning(); } protected createElement(): HTMLElement { return document.createElement('swp-event'); } /** * Create inner HTML structure */ private createInnerStructure(): void { const timeRange = TimeFormatter.formatTimeRange(this.event.start, this.event.end); const durationMinutes = (this.event.end.getTime() - this.event.start.getTime()) / (1000 * 60); this.element.innerHTML = ` ${timeRange} ${this.event.title} `; } /** * Apply positioning styles */ private applyPositioning(): void { const position = this.calculateEventPosition(); this.element.style.position = 'absolute'; this.element.style.top = `${position.top + 1}px`; this.element.style.height = `${position.height - 3}px`; this.element.style.left = '2px'; this.element.style.right = '2px'; } /** * Factory method to create a SwpEventElement from a CalendarEvent */ public static fromCalendarEvent(event: CalendarEvent): SwpEventElement { return new SwpEventElement(event); } /** * Create a clone of this SwpEventElement with "clone-" prefix */ public createClone(): SwpEventElement { // Clone the underlying DOM element const clonedElement = this.element.cloneNode(true) as HTMLElement; // Create new SwpEventElement instance from the cloned DOM const clonedSwpEvent = SwpEventElement.fromExistingElement(clonedElement); // Apply "clone-" prefix to ID clonedSwpEvent.updateEventId(`clone-${this.event.id}`); // Cache original duration for drag operations const originalDuration = this.getOriginalEventDuration(); clonedSwpEvent.element.dataset.originalDuration = originalDuration.toString(); // Set height from original element clonedSwpEvent.element.style.height = this.element.style.height || `${this.element.getBoundingClientRect().height}px`; return clonedSwpEvent; } /** * Factory method to create SwpEventElement from existing DOM element */ public static fromExistingElement(element: HTMLElement): SwpEventElement { // Extract CalendarEvent data from DOM element const event = this.extractCalendarEventFromElement(element); // Create new instance but replace the created element with the existing one const swpEvent = new SwpEventElement(event); swpEvent.element = element; return swpEvent; } /** * Update the event ID in both the CalendarEvent and DOM element */ private updateEventId(newId: string): void { this.event.id = newId; this.element.dataset.eventId = newId; } /** * Extract original event duration from DOM element */ private getOriginalEventDuration(): number { const timeElement = this.element.querySelector('swp-event-time'); if (timeElement) { const duration = timeElement.getAttribute('data-duration'); if (duration) { return parseInt(duration); } } return 60; // Fallback } /** * Extract CalendarEvent from DOM element */ private static extractCalendarEventFromElement(element: HTMLElement): CalendarEvent { return { id: element.dataset.eventId || '', title: element.dataset.title || '', start: new Date(element.dataset.start || ''), end: new Date(element.dataset.end || ''), type: element.dataset.type || 'work', allDay: false, syncStatus: 'synced', metadata: { duration: element.dataset.duration } }; } /** * Factory method to convert an all-day HTML element to a timed SwpEventElement */ public static fromAllDayElement(allDayElement: HTMLElement): SwpEventElement { // Extract data from all-day element's dataset const eventId = allDayElement.dataset.eventId || ''; const title = allDayElement.dataset.title || allDayElement.textContent || 'Untitled'; const type = allDayElement.dataset.type || 'work'; const startStr = allDayElement.dataset.start; const endStr = allDayElement.dataset.end; const durationStr = allDayElement.dataset.duration; if (!startStr || !endStr) { throw new Error('All-day element missing start/end dates'); } // Parse dates and set reasonable 1-hour duration for timed event const originalStart = new Date(startStr); const duration = durationStr ? parseInt(durationStr) : 60; // Default 1 hour // For conversion, use current time or a reasonable default (9 AM) const now = new Date(); const startDate = new Date(originalStart); startDate.setHours(now.getHours() || 9, now.getMinutes() || 0, 0, 0); const endDate = new Date(startDate); endDate.setMinutes(endDate.getMinutes() + duration); // Create CalendarEvent object const calendarEvent: CalendarEvent = { id: eventId, title: title, start: startDate, end: endDate, type: type, allDay: false, syncStatus: 'synced', metadata: { duration: duration.toString() } }; return new SwpEventElement(calendarEvent); } } /** * All-day event element (swp-allday-event) */ export class SwpAllDayEventElement extends BaseEventElement { private columnIndex: number; private constructor(event: CalendarEvent, columnIndex: number) { super(event); this.columnIndex = columnIndex; this.setAllDayAttributes(); this.createInnerStructure(); this.applyGridPositioning(); } protected createElement(): HTMLElement { return document.createElement('swp-allday-event'); } /** * Set all-day specific attributes */ private setAllDayAttributes(): void { this.element.dataset.allDay = "true"; // For all-day events, preserve original start/end dates but set to full day times const startDateStr = this.event.start.toISOString().split('T')[0]; const endDateStr = this.event.end.toISOString().split('T')[0]; this.element.dataset.start = `${startDateStr}T00:00:00`; this.element.dataset.end = `${endDateStr}T23:59:59`; } /** * Create inner structure (just text content for all-day events) */ private createInnerStructure(): void { this.element.textContent = this.event.title; } /** * Apply CSS grid positioning */ private applyGridPositioning(): void { this.element.style.gridColumn = this.columnIndex.toString(); } /** * Set grid row for this all-day event */ public setGridRow(row: number): void { this.element.style.gridRow = row.toString(); } /** * Set grid column span for this all-day event */ public setColumnSpan(startColumn: number, endColumn: number): void { this.element.style.gridColumn = `${startColumn} / ${endColumn + 1}`; } /** * Factory method to create from CalendarEvent and target date */ public static fromCalendarEvent(event: CalendarEvent, targetDate?: string): SwpAllDayEventElement { // Calculate column span based on event start and end dates const { startColumn, endColumn, columnSpan } = this.calculateColumnSpan(event); // For backwards compatibility, use targetDate if provided, otherwise use calculated start column const finalStartColumn = targetDate ? this.getColumnIndexForDate(targetDate) : startColumn; const finalEndColumn = targetDate ? finalStartColumn : endColumn; const finalColumnSpan = targetDate ? 1 : columnSpan; // Find occupied rows in the spanned columns using computedStyle const existingEvents = document.querySelectorAll('swp-allday-event'); const occupiedRows = new Set(); console.log('🔍 SwpAllDayEventElement: Checking grid row for new event', { targetDate, finalStartColumn, finalEndColumn, existingEventsCount: existingEvents.length }); existingEvents.forEach(existingEvent => { const style = getComputedStyle(existingEvent); const eventStartCol = parseInt(style.gridColumnStart); const eventEndCol = parseInt(style.gridColumnEnd); const eventRow = parseInt(style.gridRowStart) || 1; const eventId = (existingEvent as HTMLElement).dataset.eventId; console.log('📊 SwpAllDayEventElement: Checking existing event', { eventId, eventStartCol, eventEndCol, eventRow, newEventColumn: finalStartColumn }); // FIXED: Only check events in the same column (not overlap detection) if (eventStartCol === finalStartColumn) { console.log('✅ SwpAllDayEventElement: Same column - adding occupied row', eventRow); occupiedRows.add(eventRow); } else { console.log('⏭️ SwpAllDayEventElement: Different column - skipping'); } }); // Find first available row let targetRow = 1; while (occupiedRows.has(targetRow)) { targetRow++; } console.log('🎯 SwpAllDayEventElement: Final row assignment', { targetDate, finalStartColumn, occupiedRows: Array.from(occupiedRows).sort(), assignedRow: targetRow }); // Create element with calculated column span const element = new SwpAllDayEventElement(event, finalStartColumn); element.setGridRow(targetRow); element.setColumnSpan(finalStartColumn, finalEndColumn); console.log('✅ SwpAllDayEventElement: Created all-day event', { eventId: event.id, title: event.title, column: finalStartColumn, row: targetRow }); return element; } /** * Calculate column span based on event start and end dates */ private static calculateColumnSpan(event: CalendarEvent): { startColumn: number; endColumn: number; columnSpan: number } { const dayHeaders = document.querySelectorAll('swp-day-header'); // Extract dates from headers const headerDates: string[] = []; dayHeaders.forEach(header => { const date = (header as HTMLElement).dataset.date; if (date) { headerDates.push(date); } }); // Format event dates for comparison (YYYY-MM-DD format) const eventStartDate = event.start.toISOString().split('T')[0]; const eventEndDate = event.end.toISOString().split('T')[0]; // Find start and end column indices let startColumn = 1; let endColumn = headerDates.length; headerDates.forEach((dateStr, index) => { if (dateStr === eventStartDate) { startColumn = index + 1; } if (dateStr === eventEndDate) { endColumn = index + 1; } }); // Ensure end column is at least start column if (endColumn < startColumn) { endColumn = startColumn; } const columnSpan = endColumn - startColumn + 1; return { startColumn, endColumn, columnSpan }; } /** * Get column index for a specific date */ private static getColumnIndexForDate(targetDate: string): number { const dayHeaders = document.querySelectorAll('swp-day-header'); let columnIndex = 1; dayHeaders.forEach((header, index) => { if ((header as HTMLElement).dataset.date === targetDate) { columnIndex = index + 1; } }); return columnIndex; } /** * Check if two column ranges overlap */ private static columnsOverlap(startA: number, endA: number, startB: number, endB: number): boolean { return !(endA < startB || endB < startA); } }