diff --git a/src/data/mock-events.json b/src/data/mock-events.json index 192190f..ff80c44 100644 --- a/src/data/mock-events.json +++ b/src/data/mock-events.json @@ -692,7 +692,7 @@ { "id": "70", "title": "Summer Festival", - "start": "2025-08-15T00:00:00", + "start": "2025-08-14T00:00:00", "end": "2025-08-16T23:59:59", "type": "milestone", "allDay": true, diff --git a/src/managers/CalendarManager.ts b/src/managers/CalendarManager.ts index 173e3df..551fe54 100644 --- a/src/managers/CalendarManager.ts +++ b/src/managers/CalendarManager.ts @@ -74,6 +74,21 @@ export class CalendarManager { } await this.gridManager.render(); + // Step 2b: Trigger event rendering now that data is loaded + // Re-emit GRID_RENDERED to trigger EventRendererManager + console.log('๐ŸŽจ Triggering event rendering...'); + const gridContainer = document.querySelector('swp-calendar-container'); + if (gridContainer) { + const periodRange = this.gridManager.getDisplayDates(); + this.eventBus.emit(CoreEvents.GRID_RENDERED, { + container: gridContainer, + currentDate: new Date(), + startDate: periodRange[0], + endDate: periodRange[periodRange.length - 1], + columnCount: periodRange.length + }); + } + // Step 3: Initialize scroll synchronization console.log('๐Ÿ“œ Setting up scroll synchronization...'); this.scrollManager.initialize(); diff --git a/src/managers/EventManager.ts b/src/managers/EventManager.ts index 2f7e79b..8e2770f 100644 --- a/src/managers/EventManager.ts +++ b/src/managers/EventManager.ts @@ -122,6 +122,7 @@ export class EventManager { * Get events for a specific time period */ public getEventsForPeriod(startDate: Date, endDate: Date): CalendarEvent[] { + console.log(`EventManager.getEventsForPeriod: Checking ${this.events.length} events for period ${startDate.toDateString()} - ${endDate.toDateString()}`); return this.events.filter(event => { const eventStart = new Date(event.start); const eventEnd = new Date(event.end); diff --git a/src/managers/GridManager.ts b/src/managers/GridManager.ts index f751751..4d82bbc 100644 --- a/src/managers/GridManager.ts +++ b/src/managers/GridManager.ts @@ -7,7 +7,6 @@ import { eventBus } from '../core/EventBus'; import { calendarConfig } from '../core/CalendarConfig'; import { CoreEvents } from '../constants/CoreEvents'; import { ResourceCalendarData, CalendarView } from '../types/CalendarTypes'; -import { AllDayEvent } from '../types/EventTypes'; import { ViewStrategy, ViewContext } from '../strategies/ViewStrategy'; import { WeekViewStrategy } from '../strategies/WeekViewStrategy'; import { MonthViewStrategy } from '../strategies/MonthViewStrategy'; @@ -18,7 +17,6 @@ import { MonthViewStrategy } from '../strategies/MonthViewStrategy'; export class GridManager { private container: HTMLElement | null = null; private currentDate: Date = new Date(); - private allDayEvents: AllDayEvent[] = []; private resourceData: ResourceCalendarData | null = null; private currentStrategy: ViewStrategy; private eventCleanup: (() => void)[] = []; @@ -53,15 +51,6 @@ export class GridManager { }) ); - // Listen for data changes - - this.eventCleanup.push( - eventBus.on(CoreEvents.DATA_LOADED, (e: Event) => { - const detail = (e as CustomEvent).detail; - this.updateAllDayEvents(detail.events); - }) - ); - // Listen for config changes that affect rendering this.eventCleanup.push( eventBus.on(CoreEvents.REFRESH_REQUESTED, (e: Event) => { @@ -127,7 +116,6 @@ export class GridManager { const context: ViewContext = { currentDate: this.currentDate, container: this.container, - allDayEvents: this.allDayEvents, resourceData: this.resourceData }; @@ -155,14 +143,6 @@ export class GridManager { console.log(`โœ… Grid rendered with ${layoutConfig.columnCount} columns`); } - /** - * Update all-day events and re-render - */ - private updateAllDayEvents(events: AllDayEvent[]): void { - console.log(`GridManager: Updating ${events.length} all-day events`); - this.allDayEvents = events.filter(event => event.allDay); - this.render(); - } /** * Get current period label from strategy @@ -239,7 +219,6 @@ export class GridManager { // Clear references this.container = null; - this.allDayEvents = []; this.resourceData = null; } } \ No newline at end of file diff --git a/src/renderers/EventRenderer.ts b/src/renderers/EventRenderer.ts index e0d229b..1fc2b6a 100644 --- a/src/renderers/EventRenderer.ts +++ b/src/renderers/EventRenderer.ts @@ -29,16 +29,22 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { // clearEvents() would remove events from all containers, breaking the animation // Events are now rendered directly into the new container without clearing - // Events should already be filtered by EventManager - no need to filter here - console.log('BaseEventRenderer: Rendering', events.length, 'pre-filtered events'); + // Separate all-day events from regular events + const allDayEvents = events.filter(event => event.allDay); + const regularEvents = events.filter(event => !event.allDay); + + console.log(`BaseEventRenderer: Rendering ${allDayEvents.length} all-day events and ${regularEvents.length} regular events`); - // Find columns in the specific container + // Always call renderAllDayEvents to ensure height is set correctly (even to 0) + this.renderAllDayEvents(allDayEvents, container, config); + + // Find columns in the specific container for regular events const columns = this.getColumns(container); console.log(`BaseEventRenderer: Found ${columns.length} columns in container`); columns.forEach(column => { - const columnEvents = this.getEventsForColumn(column, events); - console.log(`BaseEventRenderer: Rendering ${columnEvents.length} events in column`); + const columnEvents = this.getEventsForColumn(column, regularEvents); + console.log(`BaseEventRenderer: Rendering ${columnEvents.length} regular events in column`); const eventsLayer = column.querySelector('swp-events-layer'); if (eventsLayer) { @@ -60,6 +66,143 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { protected abstract getColumns(container: HTMLElement): HTMLElement[]; protected abstract getEventsForColumn(column: HTMLElement, events: CalendarEvent[]): CalendarEvent[]; + /** + * Render all-day events in the header row 2 + */ + protected renderAllDayEvents(allDayEvents: CalendarEvent[], container: HTMLElement, config: CalendarConfig): void { + console.log(`BaseEventRenderer: Rendering ${allDayEvents.length} all-day events`); + + // Find the calendar header + const calendarHeader = container.querySelector('swp-calendar-header'); + if (!calendarHeader) { + console.warn('BaseEventRenderer: No calendar header found for all-day events'); + return; + } + + // Clear any existing all-day containers first + const existingContainers = calendarHeader.querySelectorAll('swp-allday-container'); + existingContainers.forEach(container => container.remove()); + + // Track maximum number of stacked events to calculate row height + let maxStackHeight = 0; + + // Get day headers to build date map + const dayHeaders = calendarHeader.querySelectorAll('swp-day-header'); + const dateToColumnMap = new Map(); + const visibleDates: string[] = []; + + dayHeaders.forEach((header, index) => { + const dateStr = (header as any).dataset.date; + if (dateStr) { + dateToColumnMap.set(dateStr, index + 1); // 1-based column index + visibleDates.push(dateStr); + } + }); + + // Group events by their start column for container creation + const eventsByStartColumn = new Map(); + + allDayEvents.forEach(event => { + const startDate = new Date(event.start); + const startDateKey = this.dateCalculator.formatISODate(startDate); + const startColumn = dateToColumnMap.get(startDateKey); + + if (!startColumn) { + console.log(`BaseEventRenderer: Event "${event.title}" starts outside visible week`); + return; + } + + // Store event with its start column + if (!eventsByStartColumn.has(startColumn)) { + eventsByStartColumn.set(startColumn, []); + } + eventsByStartColumn.get(startColumn)!.push(event); + }); + + // Create containers and render events + eventsByStartColumn.forEach((events, startColumn) => { + events.forEach(event => { + const startDate = new Date(event.start); + const endDate = new Date(event.end); + + // Calculate span + let endColumn = startColumn; + const currentDate = new Date(startDate); + + while (currentDate <= endDate) { + currentDate.setDate(currentDate.getDate() + 1); + const dateKey = this.dateCalculator.formatISODate(currentDate); + const col = dateToColumnMap.get(dateKey); + if (col) { + endColumn = col; + } else { + break; + } + } + + const columnSpan = endColumn - startColumn + 1; + + // Create or find container for this column span + const containerKey = `${startColumn}-${columnSpan}`; + let allDayContainer = calendarHeader.querySelector(`swp-allday-container[data-container-key="${containerKey}"]`); + + if (!allDayContainer) { + // Create container that spans the appropriate columns + allDayContainer = document.createElement('swp-allday-container'); + allDayContainer.setAttribute('data-container-key', containerKey); + (allDayContainer as HTMLElement).style.gridColumn = columnSpan > 1 + ? `${startColumn} / span ${columnSpan}` + : `${startColumn}`; + (allDayContainer as HTMLElement).style.gridRow = '2'; + calendarHeader.appendChild(allDayContainer); + } + + // Create the all-day event element inside container + const allDayEvent = document.createElement('swp-allday-event'); + allDayEvent.textContent = event.title; + allDayEvent.setAttribute('data-event-id', event.id); + allDayEvent.setAttribute('data-type', event.type || 'work'); + + // Use event metadata for color if available + if (event.metadata?.color) { + (allDayEvent as HTMLElement).style.backgroundColor = event.metadata.color; + } + + console.log(`BaseEventRenderer: All-day event "${event.title}" in container spanning columns ${startColumn} to ${endColumn}`); + + allDayContainer.appendChild(allDayEvent); + + // Track max stack height + const containerEventCount = allDayContainer.querySelectorAll('swp-allday-event').length; + if (containerEventCount > maxStackHeight) { + maxStackHeight = containerEventCount; + } + }); + }); + + // Calculate and set the all-day row height based on max stack + // Each event is 22px height + 2px gap + const eventHeight = 22; + const gap = 2; + const padding = 4; // Container padding (2px top + 2px bottom) + const calculatedHeight = maxStackHeight > 0 + ? (maxStackHeight * eventHeight) + ((maxStackHeight - 1) * gap) + padding + : 0; // No height if no events + + // Set CSS variable for row height + const root = document.documentElement; + root.style.setProperty('--all-day-row-height', `${calculatedHeight}px`); + + // Also update header-spacer height + const headerSpacer = container.querySelector('swp-header-spacer'); + if (headerSpacer) { + const headerHeight = parseInt(getComputedStyle(root).getPropertyValue('--header-height') || '80'); + (headerSpacer as HTMLElement).style.height = `${headerHeight + calculatedHeight}px`; + } + + console.log(`BaseEventRenderer: Set all-day row height to ${calculatedHeight}px (max stack: ${maxStackHeight})`); + } + protected renderEvent(event: CalendarEvent, container: Element, config: CalendarConfig): void { const eventElement = document.createElement('swp-event'); eventElement.dataset.eventId = event.id; diff --git a/src/renderers/GridRenderer.ts b/src/renderers/GridRenderer.ts index 877a540..656bc79 100644 --- a/src/renderers/GridRenderer.ts +++ b/src/renderers/GridRenderer.ts @@ -3,7 +3,6 @@ import { ResourceCalendarData } from '../types/CalendarTypes'; import { CalendarTypeFactory } from '../factories/CalendarTypeFactory'; import { HeaderRenderContext } from './HeaderRenderer'; import { ColumnRenderContext } from './ColumnRenderer'; -import { AllDayEvent } from '../types/EventTypes'; /** * GridRenderer - Handles DOM rendering for the calendar grid * Separated from GridManager to follow Single Responsibility Principle @@ -21,8 +20,7 @@ export class GridRenderer { public renderGrid( grid: HTMLElement, currentWeek: Date, - resourceData: ResourceCalendarData | null, - allDayEvents: AllDayEvent[] + resourceData: ResourceCalendarData | null ): void { console.log('GridRenderer: renderGrid called', { hasGrid: !!grid, @@ -41,11 +39,11 @@ export class GridRenderer { // Create POC structure: header-spacer + time-axis + grid-container this.createHeaderSpacer(grid); this.createTimeAxis(grid); - this.createGridContainer(grid, currentWeek, resourceData, allDayEvents); + this.createGridContainer(grid, currentWeek, resourceData); } else { console.log('GridRenderer: Re-render - updating existing structure'); // Just update the calendar header for all-day events - this.updateCalendarHeader(grid, currentWeek, resourceData, allDayEvents); + this.updateCalendarHeader(grid, currentWeek, resourceData); } console.log('GridRenderer: Grid rendered successfully with POC structure'); @@ -89,14 +87,13 @@ export class GridRenderer { private createGridContainer( grid: HTMLElement, currentWeek: Date, - resourceData: ResourceCalendarData | null, - allDayEvents: AllDayEvent[] + resourceData: ResourceCalendarData | null ): void { const gridContainer = document.createElement('swp-grid-container'); // Create calendar header using Strategy Pattern const calendarHeader = document.createElement('swp-calendar-header'); - this.renderCalendarHeader(calendarHeader, currentWeek, resourceData, allDayEvents); + this.renderCalendarHeader(calendarHeader, currentWeek, resourceData); gridContainer.appendChild(calendarHeader); // Create scrollable content @@ -124,8 +121,7 @@ export class GridRenderer { private renderCalendarHeader( calendarHeader: HTMLElement, currentWeek: Date, - resourceData: ResourceCalendarData | null, - allDayEvents: AllDayEvent[] + resourceData: ResourceCalendarData | null ): void { const calendarType = this.config.getCalendarMode(); const headerRenderer = CalendarTypeFactory.getHeaderRenderer(calendarType); @@ -133,7 +129,6 @@ export class GridRenderer { const context: HeaderRenderContext = { currentWeek: currentWeek, config: this.config, - allDayEvents: allDayEvents, resourceData: resourceData }; @@ -167,8 +162,7 @@ export class GridRenderer { private updateCalendarHeader( grid: HTMLElement, currentWeek: Date, - resourceData: ResourceCalendarData | null, - allDayEvents: AllDayEvent[] + resourceData: ResourceCalendarData | null ): void { const calendarHeader = grid.querySelector('swp-calendar-header'); if (!calendarHeader) return; @@ -177,6 +171,6 @@ export class GridRenderer { calendarHeader.innerHTML = ''; // Re-render headers using Strategy Pattern - this.renderCalendarHeader(calendarHeader as HTMLElement, currentWeek, resourceData, allDayEvents); + this.renderCalendarHeader(calendarHeader as HTMLElement, currentWeek, resourceData); } } \ No newline at end of file diff --git a/src/renderers/HeaderRenderer.ts b/src/renderers/HeaderRenderer.ts index da383ae..d6bc4f3 100644 --- a/src/renderers/HeaderRenderer.ts +++ b/src/renderers/HeaderRenderer.ts @@ -17,7 +17,6 @@ export interface HeaderRenderer { export interface HeaderRenderContext { currentWeek: Date; config: CalendarConfig; - allDayEvents?: any[]; resourceData?: ResourceCalendarData | null; } @@ -53,74 +52,6 @@ export class DateHeaderRenderer implements HeaderRenderer { calendarHeader.appendChild(header); }); - - // Render all-day events in row 2 - this.renderAllDayEvents(calendarHeader, context); - } - - private renderAllDayEvents(calendarHeader: HTMLElement, context: HeaderRenderContext): void { - const { currentWeek, config, allDayEvents = [] } = context; - - const dates = this.dateCalculator.getWorkWeekDates(currentWeek); - const weekDays = config.getDateViewSettings().weekDays; - const daysToShow = dates.slice(0, weekDays); - - // TEST: Add a simple test event for Monday (column 1) - const testEvent = document.createElement('swp-allday-event'); - testEvent.textContent = 'TEST ALL-DAY EVENT'; - testEvent.style.gridColumn = '1'; - testEvent.style.gridRow = '2'; - testEvent.style.backgroundColor = 'orange'; - testEvent.style.color = 'white'; - testEvent.style.padding = '4px'; - testEvent.style.fontSize = '12px'; - testEvent.style.fontWeight = 'bold'; - calendarHeader.appendChild(testEvent); - console.log('๐Ÿงช Added test all-day event to row 2, column 1'); - - // Process each all-day event to calculate its span - allDayEvents.forEach(event => { - const startDate = new Date(event.start); - const endDate = new Date(event.end); - - // Find start and end column indices - let startColumnIndex = -1; - let endColumnIndex = -1; - - daysToShow.forEach((date, index) => { - const dateStr = this.dateCalculator.formatISODate(date); - const startDateStr = this.dateCalculator.formatISODate(startDate); - - if (dateStr === startDateStr) { - startColumnIndex = index; - } - - // For end date, we need to check if the event spans to this day - if (date <= endDate) { - endColumnIndex = index; - } - }); - - // Only render if the event starts within the visible week - if (startColumnIndex >= 0) { - // If end column is not found or is before start, default to single day - if (endColumnIndex < startColumnIndex) { - endColumnIndex = startColumnIndex; - } - - const allDayEvent = document.createElement('swp-allday-event'); - allDayEvent.textContent = event.title; - - // Set grid column span: start column (1-based) to end column + 1 (1-based) - const gridColumnStart = startColumnIndex + 1; - const gridColumnEnd = endColumnIndex + 2; - allDayEvent.style.gridColumn = `${gridColumnStart} / ${gridColumnEnd}`; - // Color is now handled by CSS classes based on event type - allDayEvent.dataset.type = event.type || 'work'; - - calendarHeader.appendChild(allDayEvent); - } - }); } diff --git a/src/strategies/ViewStrategy.ts b/src/strategies/ViewStrategy.ts index 7594ac9..578364c 100644 --- a/src/strategies/ViewStrategy.ts +++ b/src/strategies/ViewStrategy.ts @@ -3,7 +3,6 @@ * Allows clean separation between week view, month view, day view etc. */ -import { AllDayEvent } from '../types/EventTypes'; import { ResourceCalendarData } from '../types/CalendarTypes'; /** @@ -12,7 +11,6 @@ import { ResourceCalendarData } from '../types/CalendarTypes'; export interface ViewContext { currentDate: Date; container: HTMLElement; - allDayEvents: AllDayEvent[]; resourceData: ResourceCalendarData | null; } diff --git a/src/strategies/WeekViewStrategy.ts b/src/strategies/WeekViewStrategy.ts index 83dca7f..21e307f 100644 --- a/src/strategies/WeekViewStrategy.ts +++ b/src/strategies/WeekViewStrategy.ts @@ -39,8 +39,7 @@ export class WeekViewStrategy implements ViewStrategy { this.gridRenderer.renderGrid( context.container, context.currentDate, - context.resourceData, - context.allDayEvents + context.resourceData ); console.log(`Week grid rendered with ${this.getLayoutConfig().columnCount} columns`); diff --git a/wwwroot/css/calendar-base-css.css b/wwwroot/css/calendar-base-css.css index 44c0e05..b033710 100644 --- a/wwwroot/css/calendar-base-css.css +++ b/wwwroot/css/calendar-base-css.css @@ -16,7 +16,7 @@ --day-column-min-width: 250px; --week-days: 7; --header-height: 80px; - --all-day-row-height: 40px; /* Default height for all-day events row */ + --all-day-row-height: 0px; /* Default height for all-day events row */ /* Time boundaries - Default fallback values */ --day-start-hour: 0; diff --git a/wwwroot/css/calendar-layout-css.css b/wwwroot/css/calendar-layout-css.css index bce3227..8eadd27 100644 --- a/wwwroot/css/calendar-layout-css.css +++ b/wwwroot/css/calendar-layout-css.css @@ -266,10 +266,20 @@ swp-day-header[data-today="true"] swp-day-date { } -/* All-day events in row 2 */ +/* All-day event container - spans columns as needed */ +swp-allday-container { + display: grid; + grid-template-columns: 1fr; /* Single column for now, can expand later */ + grid-auto-rows: min-content; + gap: 2px; + padding: 2px; + height: 100%; + overflow: hidden; +} + +/* All-day events in containers */ swp-allday-event { - grid-row: 2; /* Row 2 only */ - height: calc(var(--all-day-row-height) - 3px); /* Dynamic height minus margin */ + height: 22px; /* Fixed height for consistent stacking */ background: #ff9800; /* Default orange background */ display: flex; align-items: center; @@ -277,16 +287,15 @@ swp-allday-event { color: #fff; font-size: 0.75rem; padding: 2px 4px; - margin: 1px; border-radius: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - border-right: 1px solid rgba(0, 0, 0, 0.1); + border-left: 3px solid rgba(0, 0, 0, 0.2); } swp-allday-event:last-child { - border-right: none; /* Remove border from last all-day event */ + margin-bottom: 0; } /* Scrollable content */