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 { 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
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 { 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 }
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue