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:
parent
7f9d0129bf
commit
8161b3c42a
3 changed files with 182 additions and 132 deletions
|
|
@ -9,6 +9,7 @@ import { ResourceRenderer } from './features/resource/ResourceRenderer';
|
||||||
import { TeamRenderer } from './features/team/TeamRenderer';
|
import { TeamRenderer } from './features/team/TeamRenderer';
|
||||||
import { DepartmentRenderer } from './features/department/DepartmentRenderer';
|
import { DepartmentRenderer } from './features/department/DepartmentRenderer';
|
||||||
import { CalendarOrchestrator } from './core/CalendarOrchestrator';
|
import { CalendarOrchestrator } from './core/CalendarOrchestrator';
|
||||||
|
import { CalendarApp } from './core/CalendarApp';
|
||||||
import { TimeAxisRenderer } from './features/timeaxis/TimeAxisRenderer';
|
import { TimeAxisRenderer } from './features/timeaxis/TimeAxisRenderer';
|
||||||
import { ScrollManager } from './core/ScrollManager';
|
import { ScrollManager } from './core/ScrollManager';
|
||||||
import { HeaderDrawerManager } from './core/HeaderDrawerManager';
|
import { HeaderDrawerManager } from './core/HeaderDrawerManager';
|
||||||
|
|
@ -220,6 +221,9 @@ export function createV2Container(): Container {
|
||||||
builder.registerType(ResizeManager).as<ResizeManager>();
|
builder.registerType(ResizeManager).as<ResizeManager>();
|
||||||
builder.registerType(EventPersistenceManager).as<EventPersistenceManager>();
|
builder.registerType(EventPersistenceManager).as<EventPersistenceManager>();
|
||||||
|
|
||||||
|
// CalendarApp - genbrugelig kalenderkomponent
|
||||||
|
builder.registerType(CalendarApp).as<CalendarApp>();
|
||||||
|
|
||||||
// Demo app
|
// Demo app
|
||||||
builder.registerType(DemoApp).as<DemoApp>();
|
builder.registerType(DemoApp).as<DemoApp>();
|
||||||
|
|
||||||
|
|
|
||||||
140
src/v2/core/CalendarApp.ts
Normal file
140
src/v2/core/CalendarApp.ts
Normal 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
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 { IndexedDBContext } from '../storage/IndexedDBContext';
|
||||||
import { DataSeeder } from '../workers/DataSeeder';
|
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 { AuditService } from '../storage/audit/AuditService';
|
||||||
import { SettingsService } from '../storage/settings/SettingsService';
|
import { CalendarApp } from '../core/CalendarApp';
|
||||||
import { ResourceService } from '../storage/resources/ResourceService';
|
import { DateService } from '../core/DateService';
|
||||||
import { ViewConfigService } from '../storage/viewconfigs/ViewConfigService';
|
|
||||||
import { IWorkweekPreset } from '../types/SettingsTypes';
|
|
||||||
|
|
||||||
export class DemoApp {
|
export class DemoApp {
|
||||||
private animator!: NavigationAnimator;
|
|
||||||
private container!: HTMLElement;
|
private container!: HTMLElement;
|
||||||
private weekOffset = 0;
|
private currentView = 'simple';
|
||||||
private currentView: 'day' | 'simple' | 'resource' | 'picker' | 'team' | 'department' = 'simple';
|
|
||||||
private workweekPreset: IWorkweekPreset | null = null;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private orchestrator: CalendarOrchestrator,
|
|
||||||
private timeAxisRenderer: TimeAxisRenderer,
|
|
||||||
private dateService: DateService,
|
|
||||||
private scrollManager: ScrollManager,
|
|
||||||
private headerDrawerManager: HeaderDrawerManager,
|
|
||||||
private indexedDBContext: IndexedDBContext,
|
private indexedDBContext: IndexedDBContext,
|
||||||
private dataSeeder: DataSeeder,
|
private dataSeeder: DataSeeder,
|
||||||
private dragDropManager: DragDropManager,
|
|
||||||
private edgeScrollManager: EdgeScrollManager,
|
|
||||||
private resizeManager: ResizeManager,
|
|
||||||
private headerDrawerRenderer: HeaderDrawerRenderer,
|
|
||||||
private eventPersistenceManager: EventPersistenceManager,
|
|
||||||
private auditService: AuditService,
|
private auditService: AuditService,
|
||||||
private settingsService: SettingsService,
|
private calendarApp: CalendarApp,
|
||||||
private resourceService: ResourceService,
|
private dateService: DateService
|
||||||
private viewConfigService: ViewConfigService
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
|
|
@ -56,145 +28,79 @@ export class DemoApp {
|
||||||
await this.dataSeeder.seedIfEmpty();
|
await this.dataSeeder.seedIfEmpty();
|
||||||
console.log('[DemoApp] Data seeding complete');
|
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;
|
this.container = document.querySelector('swp-calendar-container') as HTMLElement;
|
||||||
|
|
||||||
// NavigationAnimator har DOM-dependencies - tilladt med new
|
// Initialize CalendarApp
|
||||||
this.animator = new NavigationAnimator(
|
await this.calendarApp.init(this.container);
|
||||||
document.querySelector('swp-header-track') as HTMLElement,
|
console.log('[DemoApp] CalendarApp initialized');
|
||||||
document.querySelector('swp-content-track') as HTMLElement
|
|
||||||
);
|
|
||||||
|
|
||||||
// Render time axis (06:00 - 18:00)
|
// Setup demo UI handlers
|
||||||
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
|
|
||||||
this.setupNavigation();
|
this.setupNavigation();
|
||||||
this.setupDrawerToggle();
|
this.setupDrawerToggle();
|
||||||
this.setupViewSwitching();
|
this.setupViewSwitching();
|
||||||
|
|
||||||
// Setup resource selector for picker view
|
// Listen for calendar status events
|
||||||
await this.setupResourceSelector();
|
this.setupStatusListeners();
|
||||||
|
|
||||||
// Initial render
|
// Initial render
|
||||||
this.render();
|
this.emitRenderCommand(this.currentView);
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupNavigation(): void {
|
private setupNavigation(): void {
|
||||||
document.getElementById('btn-prev')!.onclick = () => {
|
document.getElementById('btn-prev')!.onclick = () => {
|
||||||
this.weekOffset--;
|
this.emitNavigateCommand(-1, 'right');
|
||||||
this.animator.slide('right', () => this.render());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
document.getElementById('btn-next')!.onclick = () => {
|
document.getElementById('btn-next')!.onclick = () => {
|
||||||
this.weekOffset++;
|
this.emitNavigateCommand(1, 'left');
|
||||||
this.animator.slide('left', () => this.render());
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupViewSwitching(): void {
|
private setupViewSwitching(): void {
|
||||||
// View chip buttons
|
|
||||||
const chips = document.querySelectorAll('.view-chip');
|
const chips = document.querySelectorAll('.view-chip');
|
||||||
chips.forEach(chip => {
|
chips.forEach(chip => {
|
||||||
chip.addEventListener('click', () => {
|
chip.addEventListener('click', () => {
|
||||||
// Update active state
|
|
||||||
chips.forEach(c => c.classList.remove('active'));
|
chips.forEach(c => c.classList.remove('active'));
|
||||||
chip.classList.add('active');
|
chip.classList.add('active');
|
||||||
|
|
||||||
// Switch view
|
const view = (chip as HTMLElement).dataset.view;
|
||||||
const view = (chip as HTMLElement).dataset.view as typeof this.currentView;
|
|
||||||
if (view) {
|
if (view) {
|
||||||
this.currentView = view;
|
this.currentView = view;
|
||||||
this.updateSelectorVisibility();
|
this.emitRenderCommand(view);
|
||||||
this.render();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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 {
|
private setupDrawerToggle(): void {
|
||||||
document.getElementById('btn-drawer')!.onclick = () => {
|
document.getElementById('btn-drawer')!.onclick = () => {
|
||||||
this.headerDrawerManager.toggle();
|
this.container.dispatchEvent(new CustomEvent('calendar:cmd:drawer:toggle'));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async setupResourceSelector(): Promise<void> {
|
private setupStatusListeners(): void {
|
||||||
const resources = await this.resourceService.getAll();
|
this.container.addEventListener('calendar:status:ready', () => {
|
||||||
const container = document.querySelector('.resource-checkboxes') as HTMLElement;
|
console.log('[DemoApp] Calendar ready');
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 {
|
private emitRenderCommand(viewId: string, direction?: 'left' | 'right'): void {
|
||||||
const selector = document.querySelector('swp-resource-selector');
|
this.container.dispatchEvent(new CustomEvent('calendar:cmd:render', {
|
||||||
const showSelector = this.currentView === 'picker' || this.currentView === 'day';
|
detail: { viewId, direction }
|
||||||
selector?.classList.toggle('hidden', !showSelector);
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private emitNavigateCommand(offset: number, direction: 'left' | 'right'): void {
|
||||||
|
this.container.dispatchEvent(new CustomEvent('calendar:cmd:navigate', {
|
||||||
|
detail: { offset, direction }
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue