Refactors calendar view configuration management

Decouples view configuration from DemoApp logic by:
- Introducing ViewConfigService and MockViewConfigRepository
- Moving view configuration to centralized JSON data
- Simplifying DemoApp rendering process

Improves separation of concerns and makes view configurations more maintainable
This commit is contained in:
Janus C. H. Knudsen 2025-12-16 17:13:27 +01:00
parent 6a56396721
commit 7f9d0129bf
9 changed files with 217 additions and 708 deletions

View file

@ -38,6 +38,9 @@ import { DepartmentService } from './storage/departments/DepartmentService';
import { SettingsStore } from './storage/settings/SettingsStore';
import { SettingsService } from './storage/settings/SettingsService';
import { ITenantSettings } from './types/SettingsTypes';
import { ViewConfigStore } from './storage/viewconfigs/ViewConfigStore';
import { ViewConfigService } from './storage/viewconfigs/ViewConfigService';
import { ViewConfig } from './core/ViewConfig';
// Audit
import { AuditStore } from './storage/audit/AuditStore';
@ -54,6 +57,7 @@ import { MockAuditRepository } from './repositories/MockAuditRepository';
import { MockTeamRepository } from './repositories/MockTeamRepository';
import { MockDepartmentRepository } from './repositories/MockDepartmentRepository';
import { MockSettingsRepository } from './repositories/MockSettingsRepository';
import { MockViewConfigRepository } from './repositories/MockViewConfigRepository';
// Workers
import { DataSeeder } from './workers/DataSeeder';
@ -118,6 +122,7 @@ export function createV2Container(): Container {
builder.registerType(ScheduleOverrideStore).as<IStore>();
builder.registerType(AuditStore).as<IStore>();
builder.registerType(SettingsStore).as<IStore>();
builder.registerType(ViewConfigStore).as<IStore>();
// Entity services (for DataSeeder polymorphic array)
builder.registerType(EventService).as<IEntityService<ICalendarEvent>>();
@ -148,6 +153,10 @@ export function createV2Container(): Container {
builder.registerType(SettingsService).as<IEntityService<ISync>>();
builder.registerType(SettingsService).as<SettingsService>();
builder.registerType(ViewConfigService).as<IEntityService<ViewConfig>>();
builder.registerType(ViewConfigService).as<IEntityService<ISync>>();
builder.registerType(ViewConfigService).as<ViewConfigService>();
// Repositories (for DataSeeder polymorphic array)
builder.registerType(MockEventRepository).as<IApiRepository<ICalendarEvent>>();
builder.registerType(MockEventRepository).as<IApiRepository<ISync>>();
@ -173,6 +182,9 @@ export function createV2Container(): Container {
builder.registerType(MockSettingsRepository).as<IApiRepository<ITenantSettings>>();
builder.registerType(MockSettingsRepository).as<IApiRepository<ISync>>();
builder.registerType(MockViewConfigRepository).as<IApiRepository<ViewConfig>>();
builder.registerType(MockViewConfigRepository).as<IApiRepository<ISync>>();
// Audit service (listens to ENTITY_SAVED/DELETED events automatically)
builder.registerType(AuditService).as<AuditService>();

View file

@ -1,11 +1,13 @@
import { ISync } from '../types/CalendarTypes';
export interface ViewTemplate {
id: string;
name: string;
groupingTypes: string[];
}
export interface ViewConfig {
templateId: string;
export interface ViewConfig extends ISync {
id: string; // templateId (e.g. 'day', 'simple', 'resource')
groupings: GroupingConfig[];
}

View file

@ -15,6 +15,7 @@ import { HeaderDrawerRenderer } from '../features/headerdrawer/HeaderDrawerRende
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';
export class DemoApp {
@ -23,7 +24,6 @@ export class DemoApp {
private weekOffset = 0;
private currentView: 'day' | 'simple' | 'resource' | 'picker' | 'team' | 'department' = 'simple';
private workweekPreset: IWorkweekPreset | null = null;
private selectedResourceIds: string[] = [];
constructor(
private orchestrator: CalendarOrchestrator,
@ -40,7 +40,8 @@ export class DemoApp {
private eventPersistenceManager: EventPersistenceManager,
private auditService: AuditService,
private settingsService: SettingsService,
private resourceService: ResourceService
private resourceService: ResourceService,
private viewConfigService: ViewConfigService
) {}
async init(): Promise<void> {
@ -99,72 +100,28 @@ export class DemoApp {
}
private async render(): Promise<void> {
const viewConfig = this.buildViewConfig();
await this.orchestrator.render(viewConfig, this.container);
}
private buildViewConfig(): ViewConfig {
// Use workweek preset to determine which days to show
const workDays = this.workweekPreset?.workDays || [1, 2, 3, 4, 5]; // Fallback to Mon-Fri
const dates = this.dateService.getWorkWeekDates(this.weekOffset, workDays);
const today = this.dateService.getWeekDates(this.weekOffset, 1);
switch (this.currentView) {
case 'day':
return {
templateId: 'day',
groupings: [
{ type: 'resource', values: this.selectedResourceIds, idProperty: 'resourceId' },
{ type: 'date', values: today, idProperty: 'date', derivedFrom: 'start', hideHeader: true }
]
};
case 'simple':
return {
templateId: 'simple',
groupings: [
{ type: 'date', values: dates, idProperty: 'date', derivedFrom: 'start' }
]
};
case 'resource':
return {
templateId: 'resource',
groupings: [
{ type: 'resource', values: ['EMP001', 'EMP002'], idProperty: 'resourceId' },
{ type: 'date', values: dates, idProperty: 'date', derivedFrom: 'start' }
]
};
case 'team':
return {
templateId: 'team',
groupings: [
{ type: 'team', values: ['team1', 'team2'] },
{ type: 'resource', values: ['EMP001', 'EMP002', 'EMP003', 'EMP004'], idProperty: 'resourceId', belongsTo: 'team.resourceIds' },
{ type: 'date', values: dates, idProperty: 'date', derivedFrom: 'start' }
]
};
case 'department':
return {
templateId: 'department',
groupings: [
{ type: 'department', values: ['dept-styling', 'dept-training'] },
{ type: 'resource', values: ['EMP001', 'EMP002', 'EMP003', 'EMP004', 'STUDENT001', 'STUDENT002'], idProperty: 'resourceId', belongsTo: 'department.resourceIds' },
{ type: 'date', values: dates, idProperty: 'date', derivedFrom: 'start' }
]
};
case 'picker':
return {
templateId: 'picker',
groupings: [
{ type: 'resource', values: this.selectedResourceIds, idProperty: 'resourceId' },
{ type: 'date', values: dates, idProperty: 'date', derivedFrom: 'start' }
]
};
// 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 {
@ -224,25 +181,15 @@ export class DemoApp {
// Clear existing
container.innerHTML = '';
// Create checkboxes for each resource
// 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>
<input type="checkbox" value="${r.id}" checked disabled>
${r.displayName}
`;
container.appendChild(label);
});
// Default: all selected
this.selectedResourceIds = resources.map(r => r.id);
// Event listener for checkbox changes
container.addEventListener('change', () => {
const checked = container.querySelectorAll('input:checked') as NodeListOf<HTMLInputElement>;
this.selectedResourceIds = Array.from(checked).map(cb => cb.value);
this.render();
});
}
private updateSelectorVisibility(): void {

View file

@ -0,0 +1,41 @@
import { EntityType } from '../types/CalendarTypes';
import { ViewConfig } from '../core/ViewConfig';
import { IApiRepository } from './IApiRepository';
export class MockViewConfigRepository implements IApiRepository<ViewConfig> {
public readonly entityType: EntityType = 'ViewConfig';
private readonly dataUrl = 'data/viewconfigs.json';
public async fetchAll(): Promise<ViewConfig[]> {
try {
const response = await fetch(this.dataUrl);
if (!response.ok) {
throw new Error(`Failed to load viewconfigs: ${response.status} ${response.statusText}`);
}
const rawData = await response.json();
// Ensure syncStatus is set on each config
const configs: ViewConfig[] = rawData.map((config: ViewConfig) => ({
...config,
syncStatus: config.syncStatus || 'synced'
}));
return configs;
} catch (error) {
console.error('Failed to load viewconfigs:', error);
throw error;
}
}
public async sendCreate(_config: ViewConfig): Promise<ViewConfig> {
throw new Error('MockViewConfigRepository does not support sendCreate. Mock data is read-only.');
}
public async sendUpdate(_id: string, _updates: Partial<ViewConfig>): Promise<ViewConfig> {
throw new Error('MockViewConfigRepository does not support sendUpdate. Mock data is read-only.');
}
public async sendDelete(_id: string): Promise<void> {
throw new Error('MockViewConfigRepository does not support sendDelete. Mock data is read-only.');
}
}

View file

@ -0,0 +1,18 @@
import { EntityType, IEventBus } from '../../types/CalendarTypes';
import { ViewConfig } from '../../core/ViewConfig';
import { ViewConfigStore } from './ViewConfigStore';
import { BaseEntityService } from '../BaseEntityService';
import { IndexedDBContext } from '../IndexedDBContext';
export class ViewConfigService extends BaseEntityService<ViewConfig> {
readonly storeName = ViewConfigStore.STORE_NAME;
readonly entityType: EntityType = 'ViewConfig';
constructor(context: IndexedDBContext, eventBus: IEventBus) {
super(context, eventBus);
}
async getById(id: string): Promise<ViewConfig | null> {
return this.get(id);
}
}

View file

@ -0,0 +1,10 @@
import { IStore } from '../IStore';
export class ViewConfigStore implements IStore {
static readonly STORE_NAME = 'viewconfigs';
readonly storeName = ViewConfigStore.STORE_NAME;
create(db: IDBDatabase): void {
db.createObjectStore(ViewConfigStore.STORE_NAME, { keyPath: 'id' });
}
}

View file

@ -6,7 +6,7 @@ import { IWeekSchedule } from './ScheduleTypes';
export type SyncStatus = 'synced' | 'pending' | 'error';
export type EntityType = 'Event' | 'Booking' | 'Customer' | 'Resource' | 'Team' | 'Department' | 'Audit' | 'Settings';
export type EntityType = 'Event' | 'Booking' | 'Customer' | 'Resource' | 'Team' | 'Department' | 'Audit' | 'Settings' | 'ViewConfig';
/**
* CalendarEventType - Used by ICalendarEvent.type