Implements strategy pattern for calendar views
Refactors the grid management to use a strategy pattern, allowing for different calendar views (week, month, day) to be rendered using separate strategy implementations. This approach improves code organization, reduces complexity within the main grid manager, and makes it easier to add new view types in the future. The strategy pattern centralizes view-specific logic, improves testability, and reduces code duplication. A month view strategy has been added and is now selectable via UI.
This commit is contained in:
parent
3ddc6352f2
commit
414ef1caaf
6 changed files with 527 additions and 252 deletions
|
|
@ -1,294 +1,256 @@
|
||||||
// Grid structure management - Simple CSS Grid Implementation with Strategy Pattern
|
/**
|
||||||
|
* GridManager - Simplified grid manager using Strategy Pattern
|
||||||
|
* Now delegates view-specific logic to strategy implementations
|
||||||
|
*/
|
||||||
|
|
||||||
import { eventBus } from '../core/EventBus';
|
import { eventBus } from '../core/EventBus';
|
||||||
import { calendarConfig } from '../core/CalendarConfig';
|
import { calendarConfig } from '../core/CalendarConfig';
|
||||||
import { EventTypes } from '../constants/EventTypes';
|
import { EventTypes } from '../constants/EventTypes';
|
||||||
import { DateCalculator } from '../utils/DateCalculator';
|
import { CoreEvents } from '../constants/CoreEvents';
|
||||||
import { ResourceCalendarData } from '../types/CalendarTypes';
|
import { ResourceCalendarData, CalendarView } from '../types/CalendarTypes';
|
||||||
import { AllDayEvent } from '../types/EventTypes';
|
import { AllDayEvent } from '../types/EventTypes';
|
||||||
import { GridRenderer } from '../renderers/GridRenderer';
|
import { ViewStrategy, ViewContext } from '../strategies/ViewStrategy';
|
||||||
import { GridStyleManager } from '../renderers/GridStyleManager';
|
import { WeekViewStrategy } from '../strategies/WeekViewStrategy';
|
||||||
|
import { MonthViewStrategy } from '../strategies/MonthViewStrategy';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Grid position interface
|
* Simplified GridManager focused on coordination, not implementation
|
||||||
*/
|
|
||||||
interface GridPosition {
|
|
||||||
minutes: number;
|
|
||||||
time: string;
|
|
||||||
y: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manages the calendar grid structure using simple CSS Grid with Strategy Pattern
|
|
||||||
*/
|
*/
|
||||||
export class GridManager {
|
export class GridManager {
|
||||||
private container: HTMLElement | null = null;
|
private container: HTMLElement | null = null;
|
||||||
private grid: HTMLElement | null = null;
|
private currentDate: Date = new Date();
|
||||||
private currentWeek: Date | null = null;
|
private allDayEvents: AllDayEvent[] = [];
|
||||||
private allDayEvents: AllDayEvent[] = []; // Store all-day events for current week
|
private resourceData: ResourceCalendarData | null = null;
|
||||||
private resourceData: ResourceCalendarData | null = null; // Store resource data for resource calendar
|
private currentStrategy: ViewStrategy;
|
||||||
private gridRenderer: GridRenderer;
|
private eventCleanup: (() => void)[] = [];
|
||||||
private styleManager: GridStyleManager;
|
|
||||||
private dateCalculator: DateCalculator;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
console.log('🏗️ GridManager: Constructor called');
|
console.log('🏗️ GridManager: Constructor called with Strategy Pattern');
|
||||||
this.gridRenderer = new GridRenderer(calendarConfig);
|
|
||||||
this.styleManager = new GridStyleManager(calendarConfig);
|
// Default to week view strategy
|
||||||
this.dateCalculator = new DateCalculator(calendarConfig);
|
this.currentStrategy = new WeekViewStrategy();
|
||||||
|
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
private init(): void {
|
private init(): void {
|
||||||
this.findElements();
|
this.findElements();
|
||||||
this.subscribeToEvents();
|
this.subscribeToEvents();
|
||||||
|
|
||||||
// Set initial current week to today if not set
|
console.log('GridManager: Initialized with strategy pattern');
|
||||||
if (!this.currentWeek) {
|
|
||||||
this.currentWeek = this.dateCalculator.getISOWeekStart(new Date());
|
|
||||||
console.log('GridManager: Set initial currentWeek to', this.currentWeek);
|
|
||||||
// Don't render immediately - wait for proper initialization event
|
|
||||||
console.log('GridManager: Waiting for initialization complete before rendering');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getWeekStart(date: Date): Date {
|
|
||||||
// Use DateCalculator for ISO week (Monday = start)
|
|
||||||
return this.dateCalculator.getISOWeekStart(date);
|
|
||||||
}
|
|
||||||
|
|
||||||
private findElements(): void {
|
private findElements(): void {
|
||||||
this.grid = document.querySelector('swp-calendar-container');
|
this.container = document.querySelector('swp-calendar-container');
|
||||||
console.log('GridManager: findElements called, found swp-calendar-container:', !!this.grid);
|
console.log('GridManager: Found container:', !!this.container);
|
||||||
}
|
}
|
||||||
|
|
||||||
private subscribeToEvents(): void {
|
private subscribeToEvents(): void {
|
||||||
// State-driven events removed - render() is now called directly by CalendarManager
|
// Listen for view changes to switch strategies
|
||||||
|
this.eventCleanup.push(
|
||||||
// Re-render grid on config changes
|
eventBus.on(EventTypes.VIEW_CHANGED, (e: Event) => {
|
||||||
eventBus.on(EventTypes.CONFIG_UPDATE, (e: Event) => {
|
const detail = (e as CustomEvent).detail;
|
||||||
const detail = (e as CustomEvent).detail;
|
this.switchViewStrategy(detail.currentView);
|
||||||
if (['dayStartHour', 'dayEndHour', 'hourHeight', 'view', 'weekDays', 'fitToWidth'].includes(detail.key)) {
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Listen for data changes
|
||||||
|
this.eventCleanup.push(
|
||||||
|
eventBus.on(EventTypes.DATE_CHANGED, (e: Event) => {
|
||||||
|
const detail = (e as CustomEvent).detail;
|
||||||
|
this.currentDate = detail.currentDate;
|
||||||
this.render();
|
this.render();
|
||||||
}
|
})
|
||||||
});
|
);
|
||||||
|
|
||||||
// Re-render on calendar type change
|
this.eventCleanup.push(
|
||||||
eventBus.on(EventTypes.CALENDAR_TYPE_CHANGED, () => {
|
eventBus.on(EventTypes.WEEK_CHANGED, (e: Event) => {
|
||||||
this.render();
|
const detail = (e as CustomEvent).detail;
|
||||||
});
|
this.currentDate = detail.weekStart;
|
||||||
|
this.render();
|
||||||
// Re-render on view change
|
})
|
||||||
eventBus.on(EventTypes.VIEW_CHANGE, () => {
|
);
|
||||||
this.render();
|
|
||||||
});
|
this.eventCleanup.push(
|
||||||
|
eventBus.on(EventTypes.EVENTS_LOADED, (e: Event) => {
|
||||||
// Re-render on period change
|
const detail = (e as CustomEvent).detail;
|
||||||
eventBus.on(EventTypes.PERIOD_CHANGE, (e: Event) => {
|
this.updateAllDayEvents(detail.events);
|
||||||
const detail = (e as CustomEvent).detail;
|
})
|
||||||
this.currentWeek = this.dateCalculator.getISOWeekStart(detail.week);
|
);
|
||||||
this.render();
|
|
||||||
});
|
// Listen for config changes that affect rendering
|
||||||
|
this.eventCleanup.push(
|
||||||
// Handle week changes from NavigationManager
|
eventBus.on(EventTypes.CONFIG_UPDATE, (e: Event) => {
|
||||||
eventBus.on(EventTypes.WEEK_CHANGED, (e: Event) => {
|
this.render();
|
||||||
const detail = (e as CustomEvent).detail;
|
})
|
||||||
this.currentWeek = this.dateCalculator.getISOWeekStart(detail.weekStart);
|
);
|
||||||
this.render();
|
|
||||||
});
|
this.eventCleanup.push(
|
||||||
|
eventBus.on(EventTypes.WORKWEEK_CHANGED, () => {
|
||||||
// Handle date changes from CalendarManager
|
this.render();
|
||||||
eventBus.on(EventTypes.DATE_CHANGED, (e: Event) => {
|
})
|
||||||
const detail = (e as CustomEvent).detail;
|
);
|
||||||
this.currentWeek = this.dateCalculator.getISOWeekStart(detail.currentDate);
|
|
||||||
this.render();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// Handle events loaded
|
|
||||||
eventBus.on(EventTypes.EVENTS_LOADED, (e: Event) => {
|
|
||||||
const detail = (e as CustomEvent).detail;
|
|
||||||
this.updateAllDayEvents(detail.events);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// Handle grid clicks
|
|
||||||
this.setupGridInteractions();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch to a different view strategy
|
||||||
|
*/
|
||||||
|
public switchViewStrategy(view: CalendarView): void {
|
||||||
|
console.log(`GridManager: Switching to ${view} strategy`);
|
||||||
|
|
||||||
|
// Clean up current strategy
|
||||||
|
this.currentStrategy.destroy();
|
||||||
|
|
||||||
|
// Create new strategy based on view
|
||||||
|
switch (view) {
|
||||||
|
case 'week':
|
||||||
|
case 'day':
|
||||||
|
this.currentStrategy = new WeekViewStrategy();
|
||||||
|
break;
|
||||||
|
case 'month':
|
||||||
|
this.currentStrategy = new MonthViewStrategy();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn(`GridManager: Unknown view type ${view}, defaulting to week`);
|
||||||
|
this.currentStrategy = new WeekViewStrategy();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-render with new strategy
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set resource data for resource calendar mode
|
* Set resource data for resource calendar mode
|
||||||
*/
|
*/
|
||||||
public setResourceData(resourceData: ResourceCalendarData | null): void {
|
public setResourceData(resourceData: ResourceCalendarData | null): void {
|
||||||
this.resourceData = resourceData;
|
this.resourceData = resourceData;
|
||||||
console.log('GridManager: Set resource data:', resourceData ? `${resourceData.resources.length} resources` : 'null');
|
console.log('GridManager: Updated resource data');
|
||||||
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the complete grid structure - now returns Promise for direct calls
|
* Main render method - delegates to current strategy
|
||||||
*/
|
*/
|
||||||
async render(): Promise<void> {
|
public async render(): Promise<void> {
|
||||||
if (!this.grid) {
|
if (!this.container) {
|
||||||
console.warn('GridManager: render() called but this.grid is null, re-finding elements');
|
console.warn('GridManager: No container found, cannot render');
|
||||||
this.findElements();
|
return;
|
||||||
if (!this.grid) {
|
|
||||||
throw new Error('GridManager: swp-calendar-container not found, cannot render');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.group(`🏗️ GRID RENDER: ${this.currentWeek?.toDateString()}`);
|
|
||||||
console.log('Updating grid styles and rendering structure...');
|
|
||||||
|
|
||||||
this.styleManager.updateGridStyles(this.resourceData);
|
console.group(`🎨 GRID RENDER: ${this.currentDate.toDateString()}`);
|
||||||
this.gridRenderer.renderGrid(this.grid, this.currentWeek!, this.resourceData, this.allDayEvents);
|
console.log(`Using strategy: ${this.currentStrategy.constructor.name}`);
|
||||||
|
|
||||||
const columnCount = this.styleManager.getColumnCount(this.resourceData);
|
// Create context for strategy
|
||||||
console.log(`Grid structure complete - ${columnCount} columns created`);
|
const context: ViewContext = {
|
||||||
|
currentDate: this.currentDate,
|
||||||
|
container: this.container,
|
||||||
|
allDayEvents: this.allDayEvents,
|
||||||
|
resourceData: this.resourceData
|
||||||
|
};
|
||||||
|
|
||||||
// Emit GRID_RENDERED event to trigger event rendering
|
// Delegate to current strategy
|
||||||
const weekEnd = this.currentWeek ? new Date(this.currentWeek.getTime() + 6 * 24 * 60 * 60 * 1000) : null;
|
this.currentStrategy.renderGrid(context);
|
||||||
eventBus.emit(EventTypes.GRID_RENDERED, {
|
|
||||||
container: this.grid,
|
// Get layout info from strategy
|
||||||
currentWeek: this.currentWeek,
|
const layoutConfig = this.currentStrategy.getLayoutConfig();
|
||||||
startDate: this.currentWeek,
|
|
||||||
endDate: weekEnd,
|
// Emit grid rendered event
|
||||||
columnCount: columnCount
|
eventBus.emit(CoreEvents.GRID_RENDERED, {
|
||||||
|
container: this.container,
|
||||||
|
currentDate: this.currentDate,
|
||||||
|
layoutConfig: layoutConfig,
|
||||||
|
columnCount: layoutConfig.columnCount
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(`Grid rendered with ${layoutConfig.columnCount} columns`);
|
||||||
console.groupEnd();
|
console.groupEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Column count calculation moved to GridStyleManager
|
|
||||||
|
|
||||||
// Grid rendering methods moved to GridRenderer
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update all-day events data and re-render if needed
|
* Update all-day events and re-render
|
||||||
*/
|
*/
|
||||||
private updateAllDayEvents(events: AllDayEvent[]): void {
|
private updateAllDayEvents(events: AllDayEvent[]): void {
|
||||||
if (!this.currentWeek) return;
|
console.log(`GridManager: Updating ${events.length} all-day events`);
|
||||||
|
this.allDayEvents = events.filter(event => event.allDay);
|
||||||
// Filter all-day events for current week
|
this.render();
|
||||||
const weekStart = this.currentWeek;
|
|
||||||
const weekEnd = new Date(weekStart);
|
|
||||||
weekEnd.setDate(weekStart.getDate() + 6);
|
|
||||||
|
|
||||||
this.allDayEvents = events.filter(event => {
|
|
||||||
if (!event.allDay) return false;
|
|
||||||
|
|
||||||
const eventDate = new Date(event.start);
|
|
||||||
return eventDate >= weekStart && eventDate <= weekEnd;
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('GridManager: Updated all-day events:', this.allDayEvents.length);
|
|
||||||
|
|
||||||
// Update only the calendar header if grid is already rendered
|
|
||||||
if (this.grid && this.grid.children.length > 0) {
|
|
||||||
this.gridRenderer.renderGrid(this.grid, this.currentWeek!, this.resourceData, this.allDayEvents);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CSS management methods moved to GridStyleManager
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setup grid interaction handlers for POC structure
|
|
||||||
*/
|
|
||||||
private setupGridInteractions(): void {
|
|
||||||
if (!this.grid) return;
|
|
||||||
|
|
||||||
// Click handler for day columns (works for both date and resource columns)
|
|
||||||
this.grid.addEventListener('click', (e: MouseEvent) => {
|
|
||||||
// Ignore if clicking on an event
|
|
||||||
if ((e.target as Element).closest('swp-event')) return;
|
|
||||||
|
|
||||||
const dayColumn = (e.target as Element).closest('swp-day-column, swp-resource-column') as HTMLElement;
|
|
||||||
if (!dayColumn) return;
|
|
||||||
|
|
||||||
const position = this.getClickPosition(e, dayColumn);
|
|
||||||
|
|
||||||
eventBus.emit(EventTypes.GRID_CLICK, {
|
|
||||||
date: (dayColumn as any).dataset.date,
|
|
||||||
resource: (dayColumn as any).dataset.resource,
|
|
||||||
employeeId: (dayColumn as any).dataset.employeeId,
|
|
||||||
time: position.time,
|
|
||||||
minutes: position.minutes
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Double click handler for day columns
|
|
||||||
this.grid.addEventListener('dblclick', (e: MouseEvent) => {
|
|
||||||
// Ignore if clicking on an event
|
|
||||||
if ((e.target as Element).closest('swp-event')) return;
|
|
||||||
|
|
||||||
const dayColumn = (e.target as Element).closest('swp-day-column, swp-resource-column') as HTMLElement;
|
|
||||||
if (!dayColumn) return;
|
|
||||||
|
|
||||||
const position = this.getClickPosition(e, dayColumn);
|
|
||||||
|
|
||||||
eventBus.emit(EventTypes.GRID_DBLCLICK, {
|
|
||||||
date: (dayColumn as any).dataset.date,
|
|
||||||
resource: (dayColumn as any).dataset.resource,
|
|
||||||
employeeId: (dayColumn as any).dataset.employeeId,
|
|
||||||
time: position.time,
|
|
||||||
minutes: position.minutes
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get click position in day column (POC structure)
|
|
||||||
*/
|
|
||||||
private getClickPosition(event: MouseEvent, dayColumn: HTMLElement): GridPosition {
|
|
||||||
const rect = dayColumn.getBoundingClientRect();
|
|
||||||
const y = event.clientY - rect.top;
|
|
||||||
|
|
||||||
const gridSettings = calendarConfig.getGridSettings();
|
|
||||||
const hourHeight = gridSettings.hourHeight;
|
|
||||||
const minuteHeight = hourHeight / 60;
|
|
||||||
const snapInterval = gridSettings.snapInterval;
|
|
||||||
const dayStartHour = gridSettings.dayStartHour;
|
|
||||||
|
|
||||||
// Calculate total minutes from day start
|
|
||||||
let totalMinutes = Math.floor(y / minuteHeight);
|
|
||||||
|
|
||||||
// Snap to interval
|
|
||||||
totalMinutes = Math.round(totalMinutes / snapInterval) * snapInterval;
|
|
||||||
|
|
||||||
// Add day start offset
|
|
||||||
totalMinutes += dayStartHour * 60;
|
|
||||||
|
|
||||||
return {
|
|
||||||
minutes: totalMinutes,
|
|
||||||
time: this.minutesToTime(totalMinutes),
|
|
||||||
y: y
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scroll to specific hour
|
|
||||||
*/
|
|
||||||
scrollToHour(hour: number): void {
|
|
||||||
if (!this.grid) return;
|
|
||||||
|
|
||||||
const gridSettings = calendarConfig.getGridSettings();
|
|
||||||
const hourHeight = gridSettings.hourHeight;
|
|
||||||
const dayStartHour = gridSettings.dayStartHour;
|
|
||||||
const headerHeight = 80; // Header row height
|
|
||||||
const scrollTop = headerHeight + ((hour - dayStartHour) * hourHeight);
|
|
||||||
|
|
||||||
this.grid.scrollTop = scrollTop;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility methods
|
|
||||||
*/
|
|
||||||
|
|
||||||
private minutesToTime(totalMinutes: number): string {
|
/**
|
||||||
const hours = Math.floor(totalMinutes / 60);
|
* Get current period label from strategy
|
||||||
const minutes = totalMinutes % 60;
|
*/
|
||||||
const period = hours >= 12 ? 'PM' : 'AM';
|
public getCurrentPeriodLabel(): string {
|
||||||
const displayHour = hours > 12 ? hours - 12 : (hours === 0 ? 12 : hours);
|
return this.currentStrategy.getPeriodLabel(this.currentDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to next period using strategy
|
||||||
|
*/
|
||||||
|
public navigateNext(): void {
|
||||||
|
const nextDate = this.currentStrategy.getNextPeriod(this.currentDate);
|
||||||
|
this.currentDate = nextDate;
|
||||||
|
|
||||||
return `${displayHour}:${minutes.toString().padStart(2, '0')} ${period}`;
|
eventBus.emit(CoreEvents.PERIOD_CHANGED, {
|
||||||
|
direction: 'next',
|
||||||
|
newDate: nextDate,
|
||||||
|
periodLabel: this.getCurrentPeriodLabel()
|
||||||
|
});
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to previous period using strategy
|
||||||
|
*/
|
||||||
|
public navigatePrevious(): void {
|
||||||
|
const prevDate = this.currentStrategy.getPreviousPeriod(this.currentDate);
|
||||||
|
this.currentDate = prevDate;
|
||||||
|
|
||||||
|
eventBus.emit(CoreEvents.PERIOD_CHANGED, {
|
||||||
|
direction: 'previous',
|
||||||
|
newDate: prevDate,
|
||||||
|
periodLabel: this.getCurrentPeriodLabel()
|
||||||
|
});
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to today
|
||||||
|
*/
|
||||||
|
public navigateToToday(): void {
|
||||||
|
this.currentDate = new Date();
|
||||||
|
|
||||||
|
eventBus.emit(CoreEvents.DATE_CHANGED, {
|
||||||
|
newDate: this.currentDate,
|
||||||
|
periodLabel: this.getCurrentPeriodLabel()
|
||||||
|
});
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current view's display dates
|
||||||
|
*/
|
||||||
|
public getDisplayDates(): Date[] {
|
||||||
|
return this.currentStrategy.getDisplayDates(this.currentDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up all resources
|
||||||
|
*/
|
||||||
|
public destroy(): void {
|
||||||
|
console.log('GridManager: Cleaning up');
|
||||||
|
|
||||||
|
// Clean up event listeners
|
||||||
|
this.eventCleanup.forEach(cleanup => cleanup());
|
||||||
|
this.eventCleanup = [];
|
||||||
|
|
||||||
|
// Clean up current strategy
|
||||||
|
this.currentStrategy.destroy();
|
||||||
|
|
||||||
|
// Clear references
|
||||||
|
this.container = null;
|
||||||
|
this.allDayEvents = [];
|
||||||
|
this.resourceData = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { EventBus } from '../core/EventBus';
|
import { EventBus } from '../core/EventBus';
|
||||||
import { IEventBus, CalendarEvent, RenderContext } from '../types/CalendarTypes';
|
import { IEventBus, CalendarEvent, RenderContext } from '../types/CalendarTypes';
|
||||||
import { EventTypes } from '../constants/EventTypes';
|
import { EventTypes } from '../constants/EventTypes';
|
||||||
|
import { CoreEvents } from '../constants/CoreEvents';
|
||||||
import { calendarConfig } from '../core/CalendarConfig';
|
import { calendarConfig } from '../core/CalendarConfig';
|
||||||
import { CalendarTypeFactory } from '../factories/CalendarTypeFactory';
|
import { CalendarTypeFactory } from '../factories/CalendarTypeFactory';
|
||||||
import { EventManager } from '../managers/EventManager';
|
import { EventManager } from '../managers/EventManager';
|
||||||
|
|
@ -54,7 +55,13 @@ export class EventRenderingService {
|
||||||
private setupEventListeners(): void {
|
private setupEventListeners(): void {
|
||||||
// Event-driven rendering: React to grid and container events
|
// Event-driven rendering: React to grid and container events
|
||||||
this.eventBus.on(EventTypes.GRID_RENDERED, (event: Event) => {
|
this.eventBus.on(EventTypes.GRID_RENDERED, (event: Event) => {
|
||||||
console.log('EventRenderer: Received GRID_RENDERED event');
|
console.log('EventRenderer: Received GRID_RENDERED event (legacy)');
|
||||||
|
this.handleGridRendered(event as CustomEvent);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen to new CoreEvents system as well
|
||||||
|
this.eventBus.on(CoreEvents.GRID_RENDERED, (event: Event) => {
|
||||||
|
console.log('EventRenderer: Received GRID_RENDERED event (core)');
|
||||||
this.handleGridRendered(event as CustomEvent);
|
this.handleGridRendered(event as CustomEvent);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -82,16 +89,32 @@ export class EventRenderingService {
|
||||||
* Handle GRID_RENDERED event - render events in the current grid
|
* Handle GRID_RENDERED event - render events in the current grid
|
||||||
*/
|
*/
|
||||||
private handleGridRendered(event: CustomEvent): void {
|
private handleGridRendered(event: CustomEvent): void {
|
||||||
const { container, startDate, endDate } = event.detail;
|
const { container, startDate, endDate, currentDate } = event.detail;
|
||||||
|
|
||||||
if (!container) {
|
if (!container) {
|
||||||
console.error('EventRenderer: No container in GRID_RENDERED event', event.detail);
|
console.error('EventRenderer: No container in GRID_RENDERED event', event.detail);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use period from event or fallback to calculated period
|
let periodStart: Date;
|
||||||
const periodStart = startDate;
|
let periodEnd: Date;
|
||||||
const periodEnd = endDate;
|
|
||||||
|
if (startDate && endDate) {
|
||||||
|
// Legacy format with explicit dates
|
||||||
|
periodStart = startDate;
|
||||||
|
periodEnd = endDate;
|
||||||
|
} else if (currentDate) {
|
||||||
|
// New format - calculate period from current date
|
||||||
|
// For now, use a week period. This could be improved by getting the actual period from the strategy
|
||||||
|
const baseDate = new Date(currentDate);
|
||||||
|
periodStart = new Date(baseDate);
|
||||||
|
periodStart.setDate(baseDate.getDate() - baseDate.getDay() + 1); // Monday
|
||||||
|
periodEnd = new Date(periodStart);
|
||||||
|
periodEnd.setDate(periodStart.getDate() + 6); // Sunday
|
||||||
|
} else {
|
||||||
|
console.error('EventRenderer: No date information in GRID_RENDERED event', event.detail);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.renderEvents({
|
this.renderEvents({
|
||||||
container: container,
|
container: container,
|
||||||
|
|
|
||||||
148
src/strategies/MonthViewStrategy.ts
Normal file
148
src/strategies/MonthViewStrategy.ts
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
/**
|
||||||
|
* MonthViewStrategy - Strategy for month view rendering
|
||||||
|
* Completely different from week view - no time axis, cell-based events
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ViewStrategy, ViewContext, ViewLayoutConfig } from './ViewStrategy';
|
||||||
|
import { DateCalculator } from '../utils/DateCalculator';
|
||||||
|
import { calendarConfig } from '../core/CalendarConfig';
|
||||||
|
|
||||||
|
export class MonthViewStrategy implements ViewStrategy {
|
||||||
|
private dateCalculator: DateCalculator;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.dateCalculator = new DateCalculator(calendarConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
getLayoutConfig(): ViewLayoutConfig {
|
||||||
|
return {
|
||||||
|
needsTimeAxis: false, // No time axis in month view!
|
||||||
|
columnCount: 7, // Always 7 days (Mon-Sun)
|
||||||
|
scrollable: false, // Month fits in viewport
|
||||||
|
eventPositioning: 'cell-based' // Events go in day cells
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
renderGrid(context: ViewContext): void {
|
||||||
|
console.group(`📅 MONTH VIEW: Rendering grid for ${context.currentDate.toDateString()}`);
|
||||||
|
|
||||||
|
// Clear existing content
|
||||||
|
context.container.innerHTML = '';
|
||||||
|
|
||||||
|
// Create month grid (completely different from week!)
|
||||||
|
this.createMonthGrid(context);
|
||||||
|
|
||||||
|
console.log('Month grid rendered with 7x6 layout');
|
||||||
|
console.groupEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
private createMonthGrid(context: ViewContext): void {
|
||||||
|
const monthGrid = document.createElement('div');
|
||||||
|
monthGrid.className = 'month-grid';
|
||||||
|
monthGrid.style.display = 'grid';
|
||||||
|
monthGrid.style.gridTemplateColumns = 'repeat(7, 1fr)';
|
||||||
|
monthGrid.style.gridTemplateRows = 'auto repeat(6, 1fr)';
|
||||||
|
monthGrid.style.height = '100%';
|
||||||
|
|
||||||
|
// Add day headers (Mon, Tue, Wed, etc.)
|
||||||
|
this.createDayHeaders(monthGrid);
|
||||||
|
|
||||||
|
// Add 6 weeks of day cells
|
||||||
|
this.createDayCells(monthGrid, context.currentDate);
|
||||||
|
|
||||||
|
// Render events in day cells
|
||||||
|
this.renderMonthEvents(monthGrid, context.allDayEvents);
|
||||||
|
|
||||||
|
context.container.appendChild(monthGrid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private createDayHeaders(container: HTMLElement): void {
|
||||||
|
const dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||||
|
|
||||||
|
dayNames.forEach(dayName => {
|
||||||
|
const header = document.createElement('div');
|
||||||
|
header.className = 'month-day-header';
|
||||||
|
header.textContent = dayName;
|
||||||
|
header.style.padding = '8px';
|
||||||
|
header.style.fontWeight = 'bold';
|
||||||
|
header.style.textAlign = 'center';
|
||||||
|
header.style.borderBottom = '1px solid #e0e0e0';
|
||||||
|
container.appendChild(header);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private createDayCells(container: HTMLElement, monthDate: Date): void {
|
||||||
|
const dates = this.getMonthDates(monthDate);
|
||||||
|
|
||||||
|
dates.forEach(date => {
|
||||||
|
const cell = document.createElement('div');
|
||||||
|
cell.className = 'month-day-cell';
|
||||||
|
cell.dataset.date = this.dateCalculator.formatISODate(date);
|
||||||
|
cell.style.border = '1px solid #e0e0e0';
|
||||||
|
cell.style.minHeight = '100px';
|
||||||
|
cell.style.padding = '4px';
|
||||||
|
cell.style.position = 'relative';
|
||||||
|
|
||||||
|
// Day number
|
||||||
|
const dayNumber = document.createElement('div');
|
||||||
|
dayNumber.className = 'month-day-number';
|
||||||
|
dayNumber.textContent = date.getDate().toString();
|
||||||
|
dayNumber.style.fontWeight = 'bold';
|
||||||
|
dayNumber.style.marginBottom = '4px';
|
||||||
|
|
||||||
|
// Check if today
|
||||||
|
if (this.dateCalculator.isToday(date)) {
|
||||||
|
dayNumber.style.color = '#1976d2';
|
||||||
|
cell.style.backgroundColor = '#f5f5f5';
|
||||||
|
}
|
||||||
|
|
||||||
|
cell.appendChild(dayNumber);
|
||||||
|
container.appendChild(cell);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private getMonthDates(monthDate: Date): Date[] {
|
||||||
|
// Get first day of month
|
||||||
|
const firstOfMonth = new Date(monthDate.getFullYear(), monthDate.getMonth(), 1);
|
||||||
|
|
||||||
|
// Get Monday of the week containing first day
|
||||||
|
const startDate = this.dateCalculator.getISOWeekStart(firstOfMonth);
|
||||||
|
|
||||||
|
// Generate 42 days (6 weeks)
|
||||||
|
const dates: Date[] = [];
|
||||||
|
for (let i = 0; i < 42; i++) {
|
||||||
|
dates.push(this.dateCalculator.addDays(startDate, i));
|
||||||
|
}
|
||||||
|
|
||||||
|
return dates;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderMonthEvents(container: HTMLElement, events: any[]): void {
|
||||||
|
// TODO: Implement month event rendering
|
||||||
|
// Events will be small blocks in day cells
|
||||||
|
console.log(`MonthViewStrategy: Would render ${events.length} events`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getNextPeriod(currentDate: Date): Date {
|
||||||
|
return new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPreviousPeriod(currentDate: Date): Date {
|
||||||
|
return new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPeriodLabel(date: Date): string {
|
||||||
|
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
|
||||||
|
'July', 'August', 'September', 'October', 'November', 'December'];
|
||||||
|
|
||||||
|
return `${monthNames[date.getMonth()]} ${date.getFullYear()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDisplayDates(baseDate: Date): Date[] {
|
||||||
|
return this.getMonthDates(baseDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
console.log('MonthViewStrategy: Cleaning up');
|
||||||
|
}
|
||||||
|
}
|
||||||
67
src/strategies/ViewStrategy.ts
Normal file
67
src/strategies/ViewStrategy.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
/**
|
||||||
|
* ViewStrategy - Strategy pattern for different calendar view types
|
||||||
|
* Allows clean separation between week view, month view, day view etc.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { AllDayEvent } from '../types/EventTypes';
|
||||||
|
import { ResourceCalendarData } from '../types/CalendarTypes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Context object passed to strategy methods
|
||||||
|
*/
|
||||||
|
export interface ViewContext {
|
||||||
|
currentDate: Date;
|
||||||
|
container: HTMLElement;
|
||||||
|
allDayEvents: AllDayEvent[];
|
||||||
|
resourceData: ResourceCalendarData | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Layout configuration specific to each view type
|
||||||
|
*/
|
||||||
|
export interface ViewLayoutConfig {
|
||||||
|
needsTimeAxis: boolean;
|
||||||
|
columnCount: number;
|
||||||
|
scrollable: boolean;
|
||||||
|
eventPositioning: 'time-based' | 'cell-based';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base strategy interface for all view types
|
||||||
|
*/
|
||||||
|
export interface ViewStrategy {
|
||||||
|
/**
|
||||||
|
* Get the layout configuration for this view
|
||||||
|
*/
|
||||||
|
getLayoutConfig(): ViewLayoutConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the grid structure for this view
|
||||||
|
*/
|
||||||
|
renderGrid(context: ViewContext): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate next period for navigation
|
||||||
|
*/
|
||||||
|
getNextPeriod(currentDate: Date): Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate previous period for navigation
|
||||||
|
*/
|
||||||
|
getPreviousPeriod(currentDate: Date): Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get display label for current period
|
||||||
|
*/
|
||||||
|
getPeriodLabel(date: Date): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the dates that should be displayed in this view
|
||||||
|
*/
|
||||||
|
getDisplayDates(baseDate: Date): Date[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up any view-specific resources
|
||||||
|
*/
|
||||||
|
destroy(): void;
|
||||||
|
}
|
||||||
75
src/strategies/WeekViewStrategy.ts
Normal file
75
src/strategies/WeekViewStrategy.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
/**
|
||||||
|
* WeekViewStrategy - Strategy for week/day view rendering
|
||||||
|
* Extracts the time-based grid logic from GridManager
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ViewStrategy, ViewContext, ViewLayoutConfig } from './ViewStrategy';
|
||||||
|
import { DateCalculator } from '../utils/DateCalculator';
|
||||||
|
import { calendarConfig } from '../core/CalendarConfig';
|
||||||
|
import { GridRenderer } from '../renderers/GridRenderer';
|
||||||
|
import { GridStyleManager } from '../renderers/GridStyleManager';
|
||||||
|
|
||||||
|
export class WeekViewStrategy implements ViewStrategy {
|
||||||
|
private dateCalculator: DateCalculator;
|
||||||
|
private gridRenderer: GridRenderer;
|
||||||
|
private styleManager: GridStyleManager;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.dateCalculator = new DateCalculator(calendarConfig);
|
||||||
|
this.gridRenderer = new GridRenderer(calendarConfig);
|
||||||
|
this.styleManager = new GridStyleManager(calendarConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
getLayoutConfig(): ViewLayoutConfig {
|
||||||
|
return {
|
||||||
|
needsTimeAxis: true,
|
||||||
|
columnCount: calendarConfig.getWorkWeekSettings().totalDays,
|
||||||
|
scrollable: true,
|
||||||
|
eventPositioning: 'time-based'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
renderGrid(context: ViewContext): void {
|
||||||
|
console.group(`🗓️ WEEK VIEW: Rendering grid for ${context.currentDate.toDateString()}`);
|
||||||
|
|
||||||
|
// Update grid styles
|
||||||
|
this.styleManager.updateGridStyles(context.resourceData);
|
||||||
|
|
||||||
|
// Render the grid structure (time axis + day columns)
|
||||||
|
this.gridRenderer.renderGrid(
|
||||||
|
context.container,
|
||||||
|
context.currentDate,
|
||||||
|
context.resourceData,
|
||||||
|
context.allDayEvents
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`Week grid rendered with ${this.getLayoutConfig().columnCount} columns`);
|
||||||
|
console.groupEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
getNextPeriod(currentDate: Date): Date {
|
||||||
|
return this.dateCalculator.addWeeks(currentDate, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPreviousPeriod(currentDate: Date): Date {
|
||||||
|
return this.dateCalculator.addWeeks(currentDate, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPeriodLabel(date: Date): string {
|
||||||
|
const weekStart = this.dateCalculator.getISOWeekStart(date);
|
||||||
|
const weekEnd = this.dateCalculator.addDays(weekStart, 6);
|
||||||
|
const weekNumber = this.dateCalculator.getWeekNumber(date);
|
||||||
|
|
||||||
|
return `Week ${weekNumber}: ${this.dateCalculator.formatDateRange(weekStart, weekEnd)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDisplayDates(baseDate: Date): Date[] {
|
||||||
|
return this.dateCalculator.getWorkWeekDates(baseDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
// Clean up any week-specific resources
|
||||||
|
// For now, just log
|
||||||
|
console.log('WeekViewStrategy: Cleaning up');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -46,7 +46,7 @@
|
||||||
<swp-view-selector>
|
<swp-view-selector>
|
||||||
<swp-view-button data-view="day">Day</swp-view-button>
|
<swp-view-button data-view="day">Day</swp-view-button>
|
||||||
<swp-view-button data-view="week" data-active="true">Week</swp-view-button>
|
<swp-view-button data-view="week" data-active="true">Week</swp-view-button>
|
||||||
<swp-view-button data-view="month" disabled>Month</swp-view-button>
|
<swp-view-button data-view="month">Month</swp-view-button>
|
||||||
</swp-view-selector>
|
</swp-view-selector>
|
||||||
|
|
||||||
<swp-workweek-presets>
|
<swp-workweek-presets>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue