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
This commit is contained in:
Janus C. H. Knudsen 2025-12-16 22:37:35 +01:00
parent 7f9d0129bf
commit 8161b3c42a
3 changed files with 182 additions and 132 deletions

View file

@ -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<ResizeManager>();
builder.registerType(EventPersistenceManager).as<EventPersistenceManager>();
// CalendarApp - genbrugelig kalenderkomponent
builder.registerType(CalendarApp).as<CalendarApp>();
// Demo app
builder.registerType(DemoApp).as<DemoApp>();

140
src/v2/core/CalendarApp.ts Normal file
View file

@ -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<void> {
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<void> {
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<void> {
this.weekOffset += offset;
await this.animator.slide(direction, () => this.render());
this.emitStatus('rendered', { viewId: this.currentViewId });
}
private async render(): Promise<void> {
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
}));
}
}

View file

@ -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<void> {
@ -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<void> {
// 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<void> {
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 = `
<input type="checkbox" value="${r.id}" checked disabled>
${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 }
}));
}
}