From 8161b3c42a6276e0825598f238e64ca100e51880 Mon Sep 17 00:00:00 2001 From: "Janus C. H. Knudsen" Date: Tue, 16 Dec 2025 22:37:35 +0100 Subject: [PATCH] Refactors calendar application architecture Introduces CalendarApp as a reusable core component to centralize calendar rendering and navigation logic Separates concerns between core application logic and demo implementation Improves modularity and extensibility of calendar system --- src/v2/V2CompositionRoot.ts | 4 + src/v2/core/CalendarApp.ts | 140 +++++++++++++++++++++++++++++ src/v2/demo/DemoApp.ts | 170 ++++++++---------------------------- 3 files changed, 182 insertions(+), 132 deletions(-) create mode 100644 src/v2/core/CalendarApp.ts diff --git a/src/v2/V2CompositionRoot.ts b/src/v2/V2CompositionRoot.ts index 1814b18..5950e06 100644 --- a/src/v2/V2CompositionRoot.ts +++ b/src/v2/V2CompositionRoot.ts @@ -9,6 +9,7 @@ import { ResourceRenderer } from './features/resource/ResourceRenderer'; import { TeamRenderer } from './features/team/TeamRenderer'; import { DepartmentRenderer } from './features/department/DepartmentRenderer'; import { CalendarOrchestrator } from './core/CalendarOrchestrator'; +import { CalendarApp } from './core/CalendarApp'; import { TimeAxisRenderer } from './features/timeaxis/TimeAxisRenderer'; import { ScrollManager } from './core/ScrollManager'; import { HeaderDrawerManager } from './core/HeaderDrawerManager'; @@ -220,6 +221,9 @@ export function createV2Container(): Container { builder.registerType(ResizeManager).as(); builder.registerType(EventPersistenceManager).as(); + // CalendarApp - genbrugelig kalenderkomponent + builder.registerType(CalendarApp).as(); + // Demo app builder.registerType(DemoApp).as(); diff --git a/src/v2/core/CalendarApp.ts b/src/v2/core/CalendarApp.ts new file mode 100644 index 0000000..491bb86 --- /dev/null +++ b/src/v2/core/CalendarApp.ts @@ -0,0 +1,140 @@ +import { CalendarOrchestrator } from './CalendarOrchestrator'; +import { TimeAxisRenderer } from '../features/timeaxis/TimeAxisRenderer'; +import { NavigationAnimator } from './NavigationAnimator'; +import { DateService } from './DateService'; +import { ScrollManager } from './ScrollManager'; +import { HeaderDrawerManager } from './HeaderDrawerManager'; +import { ViewConfig } from './ViewConfig'; +import { DragDropManager } from '../managers/DragDropManager'; +import { EdgeScrollManager } from '../managers/EdgeScrollManager'; +import { ResizeManager } from '../managers/ResizeManager'; +import { EventPersistenceManager } from '../managers/EventPersistenceManager'; +import { HeaderDrawerRenderer } from '../features/headerdrawer/HeaderDrawerRenderer'; +import { SettingsService } from '../storage/settings/SettingsService'; +import { ResourceService } from '../storage/resources/ResourceService'; +import { ViewConfigService } from '../storage/viewconfigs/ViewConfigService'; +import { IWorkweekPreset } from '../types/SettingsTypes'; + +export class CalendarApp { + private animator!: NavigationAnimator; + private container!: HTMLElement; + private weekOffset = 0; + private currentViewId = 'simple'; + private workweekPreset: IWorkweekPreset | null = null; + + constructor( + private orchestrator: CalendarOrchestrator, + private timeAxisRenderer: TimeAxisRenderer, + private dateService: DateService, + private scrollManager: ScrollManager, + private headerDrawerManager: HeaderDrawerManager, + private dragDropManager: DragDropManager, + private edgeScrollManager: EdgeScrollManager, + private resizeManager: ResizeManager, + private headerDrawerRenderer: HeaderDrawerRenderer, + private eventPersistenceManager: EventPersistenceManager, + private settingsService: SettingsService, + private resourceService: ResourceService, + private viewConfigService: ViewConfigService + ) {} + + async init(container: HTMLElement): Promise { + this.container = container; + + // Load default workweek preset from settings + this.workweekPreset = await this.settingsService.getDefaultWorkweekPreset(); + + // Create NavigationAnimator with DOM elements + this.animator = new NavigationAnimator( + container.querySelector('swp-header-track') as HTMLElement, + container.querySelector('swp-content-track') as HTMLElement + ); + + // Render time axis (from settings later, hardcoded for now) + this.timeAxisRenderer.render( + container.querySelector('#time-axis') as HTMLElement, + 6, + 18 + ); + + // Init managers + this.scrollManager.init(container); + this.headerDrawerManager.init(container); + this.dragDropManager.init(container); + this.resizeManager.init(container); + + const scrollableContent = container.querySelector('swp-scrollable-content') as HTMLElement; + this.edgeScrollManager.init(scrollableContent); + + // Setup command event listeners + this.setupEventListeners(); + + // Emit ready status + this.emitStatus('ready'); + } + + private setupEventListeners(): void { + this.container.addEventListener('calendar:cmd:render', ((e: CustomEvent) => { + const { viewId, direction } = e.detail; + this.handleRenderCommand(viewId, direction); + }) as EventListener); + + this.container.addEventListener('calendar:cmd:navigate', ((e: CustomEvent) => { + const { offset, direction } = e.detail; + this.handleNavigateCommand(offset, direction); + }) as EventListener); + + this.container.addEventListener('calendar:cmd:drawer:toggle', (() => { + this.headerDrawerManager.toggle(); + }) as EventListener); + } + + private async handleRenderCommand(viewId: string, direction?: 'left' | 'right'): Promise { + this.currentViewId = viewId; + + if (direction) { + await this.animator.slide(direction, () => this.render()); + } else { + await this.render(); + } + + this.emitStatus('rendered', { viewId }); + } + + private async handleNavigateCommand(offset: number, direction: 'left' | 'right'): Promise { + this.weekOffset += offset; + await this.animator.slide(direction, () => this.render()); + this.emitStatus('rendered', { viewId: this.currentViewId }); + } + + private async render(): Promise { + const storedConfig = await this.viewConfigService.getById(this.currentViewId); + if (!storedConfig) { + this.emitStatus('error', { message: `ViewConfig not found: ${this.currentViewId}` }); + return; + } + + // Populate date values based on workweek and offset + const workDays = this.workweekPreset?.workDays || [1, 2, 3, 4, 5]; + const dates = this.currentViewId === 'day' + ? this.dateService.getWeekDates(this.weekOffset, 1) + : this.dateService.getWorkWeekDates(this.weekOffset, workDays); + + // Clone config and populate dates + const viewConfig: ViewConfig = { + ...storedConfig, + groupings: storedConfig.groupings.map(g => + g.type === 'date' ? { ...g, values: dates } : g + ) + }; + + await this.orchestrator.render(viewConfig, this.container); + } + + private emitStatus(status: string, detail?: object): void { + this.container.dispatchEvent(new CustomEvent(`calendar:status:${status}`, { + detail, + bubbles: true + })); + } +} diff --git a/src/v2/demo/DemoApp.ts b/src/v2/demo/DemoApp.ts index 75ffc33..2a40d52 100644 --- a/src/v2/demo/DemoApp.ts +++ b/src/v2/demo/DemoApp.ts @@ -1,47 +1,19 @@ -import { CalendarOrchestrator } from '../core/CalendarOrchestrator'; -import { TimeAxisRenderer } from '../features/timeaxis/TimeAxisRenderer'; -import { NavigationAnimator } from '../core/NavigationAnimator'; -import { DateService } from '../core/DateService'; -import { ScrollManager } from '../core/ScrollManager'; -import { HeaderDrawerManager } from '../core/HeaderDrawerManager'; import { IndexedDBContext } from '../storage/IndexedDBContext'; import { DataSeeder } from '../workers/DataSeeder'; -import { ViewConfig } from '../core/ViewConfig'; -import { DragDropManager } from '../managers/DragDropManager'; -import { EdgeScrollManager } from '../managers/EdgeScrollManager'; -import { ResizeManager } from '../managers/ResizeManager'; -import { EventPersistenceManager } from '../managers/EventPersistenceManager'; -import { HeaderDrawerRenderer } from '../features/headerdrawer/HeaderDrawerRenderer'; import { AuditService } from '../storage/audit/AuditService'; -import { SettingsService } from '../storage/settings/SettingsService'; -import { ResourceService } from '../storage/resources/ResourceService'; -import { ViewConfigService } from '../storage/viewconfigs/ViewConfigService'; -import { IWorkweekPreset } from '../types/SettingsTypes'; +import { CalendarApp } from '../core/CalendarApp'; +import { DateService } from '../core/DateService'; export class DemoApp { - private animator!: NavigationAnimator; private container!: HTMLElement; - private weekOffset = 0; - private currentView: 'day' | 'simple' | 'resource' | 'picker' | 'team' | 'department' = 'simple'; - private workweekPreset: IWorkweekPreset | null = null; + private currentView = 'simple'; constructor( - private orchestrator: CalendarOrchestrator, - private timeAxisRenderer: TimeAxisRenderer, - private dateService: DateService, - private scrollManager: ScrollManager, - private headerDrawerManager: HeaderDrawerManager, private indexedDBContext: IndexedDBContext, private dataSeeder: DataSeeder, - private dragDropManager: DragDropManager, - private edgeScrollManager: EdgeScrollManager, - private resizeManager: ResizeManager, - private headerDrawerRenderer: HeaderDrawerRenderer, - private eventPersistenceManager: EventPersistenceManager, private auditService: AuditService, - private settingsService: SettingsService, - private resourceService: ResourceService, - private viewConfigService: ViewConfigService + private calendarApp: CalendarApp, + private dateService: DateService ) {} async init(): Promise { @@ -56,145 +28,79 @@ export class DemoApp { await this.dataSeeder.seedIfEmpty(); console.log('[DemoApp] Data seeding complete'); - // Load default workweek preset from settings - this.workweekPreset = await this.settingsService.getDefaultWorkweekPreset(); - console.log('[DemoApp] Workweek preset loaded:', this.workweekPreset?.id); - this.container = document.querySelector('swp-calendar-container') as HTMLElement; - // NavigationAnimator har DOM-dependencies - tilladt med new - this.animator = new NavigationAnimator( - document.querySelector('swp-header-track') as HTMLElement, - document.querySelector('swp-content-track') as HTMLElement - ); + // Initialize CalendarApp + await this.calendarApp.init(this.container); + console.log('[DemoApp] CalendarApp initialized'); - // Render time axis (06:00 - 18:00) - this.timeAxisRenderer.render(document.getElementById('time-axis') as HTMLElement, 6, 18); - - // Init scroll synkronisering - this.scrollManager.init(this.container); - - // Init header drawer - this.headerDrawerManager.init(this.container); - - // Init drag-drop - this.dragDropManager.init(this.container); - - // Init edge scroll - const scrollableContent = this.container.querySelector('swp-scrollable-content') as HTMLElement; - this.edgeScrollManager.init(scrollableContent); - - // Init resize - this.resizeManager.init(this.container); - - // Setup event handlers + // Setup demo UI handlers this.setupNavigation(); this.setupDrawerToggle(); this.setupViewSwitching(); - // Setup resource selector for picker view - await this.setupResourceSelector(); + // Listen for calendar status events + this.setupStatusListeners(); // Initial render - this.render(); - } - - private async render(): Promise { - // Load ViewConfig from IndexedDB - const storedConfig = await this.viewConfigService.getById(this.currentView); - if (!storedConfig) { - console.error(`[DemoApp] ViewConfig not found for templateId: ${this.currentView}`); - return; - } - - // Populate date values based on workweek and offset - const workDays = this.workweekPreset?.workDays || [1, 2, 3, 4, 5]; - const dates = this.currentView === 'day' - ? this.dateService.getWeekDates(this.weekOffset, 1) - : this.dateService.getWorkWeekDates(this.weekOffset, workDays); - - // Clone config and populate dates - const viewConfig: ViewConfig = { - ...storedConfig, - groupings: storedConfig.groupings.map(g => - g.type === 'date' ? { ...g, values: dates } : g - ) - }; - - await this.orchestrator.render(viewConfig, this.container); + this.emitRenderCommand(this.currentView); } private setupNavigation(): void { document.getElementById('btn-prev')!.onclick = () => { - this.weekOffset--; - this.animator.slide('right', () => this.render()); + this.emitNavigateCommand(-1, 'right'); }; document.getElementById('btn-next')!.onclick = () => { - this.weekOffset++; - this.animator.slide('left', () => this.render()); + this.emitNavigateCommand(1, 'left'); }; } private setupViewSwitching(): void { - // View chip buttons const chips = document.querySelectorAll('.view-chip'); chips.forEach(chip => { chip.addEventListener('click', () => { - // Update active state chips.forEach(c => c.classList.remove('active')); chip.classList.add('active'); - // Switch view - const view = (chip as HTMLElement).dataset.view as typeof this.currentView; + const view = (chip as HTMLElement).dataset.view; if (view) { this.currentView = view; - this.updateSelectorVisibility(); - this.render(); + this.emitRenderCommand(view); } }); }); - - // Workweek preset dropdown - const workweekSelect = document.getElementById('workweek-select') as HTMLSelectElement; - workweekSelect?.addEventListener('change', async () => { - const presetId = workweekSelect.value; - const preset = await this.settingsService.getWorkweekPreset(presetId); - if (preset) { - this.workweekPreset = preset; - this.render(); - } - }); } private setupDrawerToggle(): void { document.getElementById('btn-drawer')!.onclick = () => { - this.headerDrawerManager.toggle(); + this.container.dispatchEvent(new CustomEvent('calendar:cmd:drawer:toggle')); }; } - private async setupResourceSelector(): Promise { - const resources = await this.resourceService.getAll(); - const container = document.querySelector('.resource-checkboxes') as HTMLElement; - if (!container) return; - - // Clear existing - container.innerHTML = ''; - - // Display resources (read-only, values are stored in ViewConfig) - resources.forEach(r => { - const label = document.createElement('label'); - label.innerHTML = ` - - ${r.displayName} - `; - container.appendChild(label); + private setupStatusListeners(): void { + this.container.addEventListener('calendar:status:ready', () => { + console.log('[DemoApp] Calendar ready'); }); + + this.container.addEventListener('calendar:status:rendered', ((e: CustomEvent) => { + console.log('[DemoApp] Calendar rendered:', e.detail.viewId); + }) as EventListener); + + this.container.addEventListener('calendar:status:error', ((e: CustomEvent) => { + console.error('[DemoApp] Calendar error:', e.detail.message); + }) as EventListener); } - private updateSelectorVisibility(): void { - const selector = document.querySelector('swp-resource-selector'); - const showSelector = this.currentView === 'picker' || this.currentView === 'day'; - selector?.classList.toggle('hidden', !showSelector); + private emitRenderCommand(viewId: string, direction?: 'left' | 'right'): void { + this.container.dispatchEvent(new CustomEvent('calendar:cmd:render', { + detail: { viewId, direction } + })); + } + + private emitNavigateCommand(offset: number, direction: 'left' | 'right'): void { + this.container.dispatchEvent(new CustomEvent('calendar:cmd:navigate', { + detail: { offset, direction } + })); } }